From bf7f8483a58b619172c96b24d19e5409951d04e1 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 11 Sep 2019 18:59:07 +0200 Subject: [PATCH 01/42] wip: scm viewlet as tree --- src/vs/base/browser/ui/tree/dataTree.ts | 3 +- src/vs/base/browser/ui/tree/tree.ts | 1 + .../contrib/scm/browser/media/scmViewlet.css | 52 +-- .../workbench/contrib/scm/browser/scmUtil.ts | 14 +- .../contrib/scm/browser/scmViewlet.ts | 332 ++++++++++-------- 5 files changed, 230 insertions(+), 172 deletions(-) diff --git a/src/vs/base/browser/ui/tree/dataTree.ts b/src/vs/base/browser/ui/tree/dataTree.ts index f9f8263655e..9771eb4a544 100644 --- a/src/vs/base/browser/ui/tree/dataTree.ts +++ b/src/vs/base/browser/ui/tree/dataTree.ts @@ -162,9 +162,10 @@ export class DataTree extends AbstractTree>(Iterator.fromArray(children), element => { const { elements: children, size } = this.iterate(element, isCollapsed); + const collapsible = this.dataSource.hasChildren ? this.dataSource.hasChildren(element) : undefined; const collapsed = size === 0 ? undefined : (isCollapsed && isCollapsed(element)); - return { element, children, collapsed }; + return { element, children, collapsible, collapsed }; }); return { elements, size: children.length }; diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index 5be49ab229f..d1fec475d71 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -167,6 +167,7 @@ export interface ITreeNavigator { } export interface IDataSource { + hasChildren?(element: TInput | T): boolean; getChildren(element: TInput | T): T[]; } diff --git a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css index d732bf2fae3..b4c0f3df1f7 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css +++ b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css @@ -60,16 +60,16 @@ } .scm-viewlet .monaco-list-row { - padding: 0 12px 0 20px; + /* padding: 0 12px 0 20px; */ line-height: 22px; } -.scm-viewlet .monaco-list-row > .resource-group { +.scm-viewlet .monaco-list-row .resource-group { display: flex; height: 100%; } -.scm-viewlet .monaco-list-row > .resource-group > .name { +.scm-viewlet .monaco-list-row .resource-group > .name { flex: 1; font-size: 11px; font-weight: bold; @@ -77,61 +77,65 @@ text-overflow: ellipsis; } -.scm-viewlet .monaco-list-row > .resource { +.scm-viewlet .monaco-list-row .resource { display: flex; height: 100%; } -.scm-viewlet .monaco-list-row > .resource.faded { +.scm-viewlet .monaco-list-row .resource.faded { opacity: 0.7; } -.scm-viewlet .monaco-list-row > .resource > .name { +.scm-viewlet .monaco-list-row .resource > .name { flex: 1; overflow: hidden; } -.scm-viewlet .monaco-list-row > .resource > .name.strike-through > .monaco-icon-label > .monaco-icon-label-description-container > .label-name { +.scm-viewlet .monaco-list-row .resource > .name.strike-through > .monaco-icon-label > .monaco-icon-label-description-container > .label-name { text-decoration: line-through; } -.scm-viewlet .monaco-list-row > .resource > .name > .monaco-icon-label::after { - padding: 0 4px; +.scm-viewlet .monaco-list-row .resource > .name > .monaco-icon-label::after { + padding: 0 8px; } -.scm-viewlet .monaco-list-row > .resource > .decoration-icon { +.scm-viewlet .monaco-list-row .resource-group > .count { + padding: 0 8px; +} + +.scm-viewlet .monaco-list-row .resource > .decoration-icon { width: 16px; height: 100%; background-repeat: no-repeat; background-position: 50% 50%; } -.scm-viewlet .monaco-list .monaco-list-row > .resource > .name > .monaco-icon-label > .actions { +.scm-viewlet .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions { flex-grow: 100; } -.scm-viewlet .monaco-list .monaco-list-row > .resource-group > .actions, -.scm-viewlet .monaco-list .monaco-list-row > .resource > .name > .monaco-icon-label > .actions { +.scm-viewlet .monaco-list .monaco-list-row .resource-group > .actions, +.scm-viewlet .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions { display: none; } -.scm-viewlet .monaco-list .monaco-list-row:hover > .resource-group > .actions, -.scm-viewlet .monaco-list .monaco-list-row:hover > .resource > .name > .monaco-icon-label > .actions, -.scm-viewlet .monaco-list .monaco-list-row.selected > .resource-group > .actions, -.scm-viewlet .monaco-list .monaco-list-row.focused > .resource-group > .actions, -.scm-viewlet .monaco-list .monaco-list-row.selected > .resource > .name > .monaco-icon-label > .actions, -.scm-viewlet .monaco-list .monaco-list-row.focused > .resource > .name > .monaco-icon-label > .actions, -.scm-viewlet .monaco-list:not(.selection-multiple) .monaco-list-row > .resource:hover > .actions { +.scm-viewlet .monaco-list .monaco-list-row:hover .resource-group > .actions, +.scm-viewlet .monaco-list .monaco-list-row:hover .resource > .name > .monaco-icon-label > .actions, +.scm-viewlet .monaco-list .monaco-list-row.selected .resource-group > .actions, +.scm-viewlet .monaco-list .monaco-list-row.focused .resource-group > .actions, +.scm-viewlet .monaco-list .monaco-list-row.selected .resource > .name > .monaco-icon-label > .actions, +.scm-viewlet .monaco-list .monaco-list-row.focused .resource > .name > .monaco-icon-label > .actions, +.scm-viewlet .monaco-list:not(.selection-multiple) .monaco-list-row .resource:hover > .actions { display: block; } -.scm-viewlet .scm-status.show-actions > .monaco-list .monaco-list-row > .resource-group > .actions, -.scm-viewlet .scm-status.show-actions > .monaco-list .monaco-list-row > .resource > .name > .monaco-icon-label > .actions { +.scm-viewlet .scm-status.show-actions > .monaco-list .monaco-list-row .resource-group > .actions, +.scm-viewlet .scm-status.show-actions > .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions { display: block; } -.scm-viewlet .monaco-list-row > .resource > .name > .monaco-icon-label > .actions .action-label, -.scm-viewlet .monaco-list-row > .resource-group > .actions .action-label { +.scm-viewlet .monaco-list-row .resource > .name > .monaco-icon-label > .actions .action-label, +.scm-viewlet .monaco-list-row .resource-group > .actions .action-label { width: 16px; height: 100%; background-position: 50% 50%; diff --git a/src/vs/workbench/contrib/scm/browser/scmUtil.ts b/src/vs/workbench/contrib/scm/browser/scmUtil.ts index 0b9307b1213..2214966835b 100644 --- a/src/vs/workbench/contrib/scm/browser/scmUtil.ts +++ b/src/vs/workbench/contrib/scm/browser/scmUtil.ts @@ -3,8 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ISCMResourceGroup, ISCMResource } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMResource, ISCMRepository, ISCMResourceGroup } from 'vs/workbench/contrib/scm/common/scm'; -export function isSCMResource(element: ISCMResourceGroup | ISCMResource): element is ISCMResource { +export function isSCMRepository(element: ISCMRepository | ISCMResourceGroup | ISCMResource): element is ISCMRepository { + return !!(element as ISCMRepository).provider && typeof (element as ISCMRepository).setSelected === 'function'; +} + +export function isSCMResourceGroup(element: ISCMRepository | ISCMResourceGroup | ISCMResource): element is ISCMRepository { + return !!(element as ISCMResourceGroup).provider && !!(element as ISCMResourceGroup).elements; +} + +export function isSCMResource(element: ISCMRepository | ISCMResourceGroup | ISCMResource): element is ISCMResource { return !!(element as ISCMResource).sourceUri; -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index e01dbff4d32..87866e09fc4 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -29,16 +29,15 @@ import { createAndFillInContextMenuActions, ContextAwareMenuEntryActionViewItem, import { SCMMenus } from './scmMenus'; import { ActionBar, IActionViewItemProvider, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService, LIGHT } from 'vs/platform/theme/common/themeService'; -import { isSCMResource } from './scmUtil'; +import { isSCMResource, isSCMRepository } from './scmUtil'; import { attachBadgeStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { Command } from 'vs/editor/common/modes'; import { renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { format } from 'vs/base/common/strings'; -import { ISpliceable, ISequence, ISplice } from 'vs/base/common/sequence'; -import { firstIndex, equals } from 'vs/base/common/arrays'; -import { WorkbenchList } from 'vs/platform/list/browser/listService'; +import { equals } from 'vs/base/common/arrays'; +import { WorkbenchList, WorkbenchDataTree } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ThrottledDelayer } from 'vs/base/common/async'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -50,6 +49,9 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IViewsRegistry, IViewDescriptor, Extensions } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { nextTick } from 'vs/base/common/process'; +import { DataTree } from 'vs/base/browser/ui/tree/dataTree'; +import { ITreeRenderer, ITreeNode, IDataSource } from 'vs/base/browser/ui/tree/tree'; +import { ISequence, ISpliceable, ISplice } from 'vs/base/common/sequence'; export interface ISpliceEvent { index: number; @@ -369,7 +371,7 @@ interface ResourceGroupTemplate { dispose: () => void; } -class ResourceGroupRenderer implements IListRenderer { +class ResourceGroupRenderer implements ITreeRenderer { static TEMPLATE_ID = 'resource group'; get templateId(): string { return ResourceGroupRenderer.TEMPLATE_ID; } @@ -398,9 +400,10 @@ class ResourceGroupRenderer implements IListRenderer, index: number, template: ResourceGroupTemplate): void { template.elementDisposable.dispose(); + const group = node.element; template.name.textContent = group.label; template.actionBar.clear(); template.actionBar.context = group; @@ -415,7 +418,7 @@ class ResourceGroupRenderer implements IListRenderer, index: number, template: ResourceGroupTemplate): void { template.elementDisposable.dispose(); } @@ -424,6 +427,31 @@ class ResourceGroupRenderer implements IListRenderer { + + static TEMPLATE_ID = 'folder'; + get templateId(): string { return FolderRenderer.TEMPLATE_ID; } + + renderTemplate(container: HTMLElement): FolderTemplate { + const element = append(container, $('.resource-folder')); + const name = append(element, $('.name')); + + return { name }; + } + + renderElement(node: ITreeNode, index: number, template: FolderTemplate): void { + template.name.textContent = node.element; + } + + disposeTemplate(): void { + // noop + } +} + interface ResourceTemplate { element: HTMLElement; name: HTMLElement; @@ -456,7 +484,7 @@ class MultipleSelectionActionRunner extends ActionRunner { } } -class ResourceRenderer implements IListRenderer { +class ResourceRenderer implements ITreeRenderer { static TEMPLATE_ID = 'resource'; get templateId(): string { return ResourceRenderer.TEMPLATE_ID; } @@ -489,9 +517,10 @@ class ResourceRenderer implements IListRenderer }; } - renderElement(resource: ISCMResource, index: number, template: ResourceTemplate): void { + renderElement(node: ITreeNode, index: number, template: ResourceTemplate): void { template.elementDisposable.dispose(); + const resource = node.element; const theme = this.themeService.getTheme(); const icon = theme.type === LIGHT ? resource.decorations.icon : resource.decorations.iconDark; @@ -518,7 +547,7 @@ class ResourceRenderer implements IListRenderer template.elementDisposable = disposables; } - disposeElement(resource: ISCMResource, index: number, template: ResourceTemplate): void { + disposeElement(resource: ITreeNode, index: number, template: ResourceTemplate): void { template.elementDisposable.dispose(); } @@ -528,15 +557,38 @@ class ResourceRenderer implements IListRenderer } } -class ProviderListDelegate implements IListVirtualDelegate { +class ProviderListDelegate implements IListVirtualDelegate { getHeight() { return 22; } - getTemplateId(element: ISCMResourceGroup | ISCMResource) { + getTemplateId(element: TreeNode) { + if (typeof element === 'string') { + return FolderRenderer.TEMPLATE_ID; + } + return isSCMResource(element) ? ResourceRenderer.TEMPLATE_ID : ResourceGroupRenderer.TEMPLATE_ID; } } +class SCMTreeDataSource implements IDataSource { + + hasChildren(element: ISCMRepository | TreeNode): boolean { + return typeof element !== 'string' && !isSCMResource(element); + } + + getChildren(element: ISCMRepository | TreeNode): TreeNode[] { + if (typeof element === 'string') { + return []; + } else if (isSCMRepository(element)) { + return element.provider.groups.elements; + } else if (isSCMResource(element)) { + return []; + } else { // ISCMResourceGroup + return element.elements; + } + } +} + const scmResourceIdentityProvider = new class implements IIdentityProvider { getId(r: ISCMResourceGroup | ISCMResource): string { if (isSCMResource(r)) { @@ -560,64 +612,38 @@ const scmKeyboardNavigationLabelProvider = new class implements IKeyboardNavigat } }; -function isGroupVisible(group: ISCMResourceGroup) { - return group.elements.length > 0 || !group.hideWhenEmpty; -} +// function isGroupVisible(group: ISCMResourceGroup) { +// return group.elements.length > 0 || !group.hideWhenEmpty; +// } interface IGroupItem { readonly group: ISCMResourceGroup; - visible: boolean; readonly disposable: IDisposable; } class ResourceGroupSplicer { private items: IGroupItem[] = []; - private disposables: IDisposable[] = []; + private disposables = new DisposableStore(); constructor( groupSequence: ISequence, - private spliceable: ISpliceable + private tree: DataTree ) { groupSequence.onDidSplice(this.onDidSpliceGroups, this, this.disposables); this.onDidSpliceGroups({ start: 0, deleteCount: 0, toInsert: groupSequence.elements }); } private onDidSpliceGroups({ start, deleteCount, toInsert }: ISplice): void { - let absoluteStart = 0; - - for (let i = 0; i < start; i++) { - const item = this.items[i]; - absoluteStart += (item.visible ? 1 : 0) + item.group.elements.length; - } - - let absoluteDeleteCount = 0; - - for (let i = 0; i < deleteCount; i++) { - const item = this.items[start + i]; - absoluteDeleteCount += (item.visible ? 1 : 0) + item.group.elements.length; - } - const itemsToInsert: IGroupItem[] = []; - const absoluteToInsert: Array = []; for (const group of toInsert) { - const visible = isGroupVisible(group); - - if (visible) { - absoluteToInsert.push(group); - } - - for (const element of group.elements) { - absoluteToInsert.push(element); - } - const disposable = combinedDisposable( - group.onDidChange(() => this.onDidChangeGroup(group)), + // group.onDidChange(() => this.onDidChangeGroup(group)), group.onDidSplice(splice => this.onDidSpliceGroup(group, splice)) ); - itemsToInsert.push({ group, visible, disposable }); + itemsToInsert.push({ group, disposable }); } const itemsToDispose = this.items.splice(start, deleteCount, ...itemsToInsert); @@ -626,69 +652,72 @@ class ResourceGroupSplicer { item.disposable.dispose(); } - this.spliceable.splice(absoluteStart, absoluteDeleteCount, absoluteToInsert); + this.tree.updateChildren(); } - private onDidChangeGroup(group: ISCMResourceGroup): void { - const itemIndex = firstIndex(this.items, item => item.group === group); + // private onDidChangeGroup(group: ISCMResourceGroup): void { + // const itemIndex = firstIndex(this.items, item => item.group === group); - if (itemIndex < 0) { - return; - } + // if (itemIndex < 0) { + // return; + // } - const item = this.items[itemIndex]; - const visible = isGroupVisible(group); + // const item = this.items[itemIndex]; + // const visible = isGroupVisible(group); - if (item.visible === visible) { - return; - } + // if (item.visible === visible) { + // return; + // } - let absoluteStart = 0; + // let absoluteStart = 0; - for (let i = 0; i < itemIndex; i++) { - const item = this.items[i]; - absoluteStart += (item.visible ? 1 : 0) + item.group.elements.length; - } + // for (let i = 0; i < itemIndex; i++) { + // const item = this.items[i]; + // absoluteStart += (item.visible ? 1 : 0) + item.group.elements.length; + // } - if (visible) { - this.spliceable.splice(absoluteStart, 0, [group, ...group.elements]); - } else { - this.spliceable.splice(absoluteStart, 1 + group.elements.length, []); - } + // if (visible) { + // this.spliceable.splice(absoluteStart, 0, [group, ...group.elements]); + // } else { + // this.spliceable.splice(absoluteStart, 1 + group.elements.length, []); + // } - item.visible = visible; - } + // item.visible = visible; + // } private onDidSpliceGroup(group: ISCMResourceGroup, { start, deleteCount, toInsert }: ISplice): void { - const itemIndex = firstIndex(this.items, item => item.group === group); + this.tree.updateChildren(group); + // const itemIndex = firstIndex(this.items, item => item.group === group); - if (itemIndex < 0) { - return; - } + // if (itemIndex < 0) { + // return; + // } - const item = this.items[itemIndex]; - const visible = isGroupVisible(group); + // const item = this.items[itemIndex]; + // const visible = isGroupVisible(group); - if (!item.visible && !visible) { - return; - } + // if (!item.visible && !visible) { + // return; + // } - let absoluteStart = start; + // let absoluteStart = start; - for (let i = 0; i < itemIndex; i++) { - const item = this.items[i]; - absoluteStart += (item.visible ? 1 : 0) + item.group.elements.length; - } + // for (let i = 0; i < itemIndex; i++) { + // const item = this.items[i]; + // absoluteStart += (item.visible ? 1 : 0) + item.group.elements.length; + // } + + // if (item.visible && !visible) { + // this.spliceable.splice(absoluteStart, 1 + deleteCount, toInsert); + // } else if (!item.visible && visible) { + // this.spliceable.splice(absoluteStart, deleteCount, [group, ...toInsert]); + // } else { + // this.spliceable.splice(absoluteStart + 1, deleteCount, toInsert); + // } + + // item.visible = visible; - if (item.visible && !visible) { - this.spliceable.splice(absoluteStart, 1 + deleteCount, toInsert); - } else if (!item.visible && visible) { - this.spliceable.splice(absoluteStart, deleteCount, [group, ...toInsert]); - } else { - this.spliceable.splice(absoluteStart + 1, deleteCount, toInsert); - } - item.visible = visible; } dispose(): void { @@ -705,6 +734,8 @@ function convertValidationType(type: InputValidationType): MessageType { } } +type TreeNode = ISCMResourceGroup | string | ISCMResource; + export class RepositoryPanel extends ViewletPanel { private cachedHeight: number | undefined = undefined; @@ -713,7 +744,7 @@ export class RepositoryPanel extends ViewletPanel { private inputBoxContainer: HTMLElement; private inputBox: InputBox; private listContainer: HTMLElement; - private list: List; + private tree: DataTree; private listLabels: ResourceLabels; private menus: SCMMenus; private visibilityDisposables: IDisposable[] = []; @@ -845,41 +876,54 @@ export class RepositoryPanel extends ViewletPanel { const renderers = [ new ResourceGroupRenderer(actionViewItemProvider, this.themeService, this.menus), + new FolderRenderer(), new ResourceRenderer(this.listLabels, actionViewItemProvider, () => this.getSelectedResources(), this.themeService, this.menus) ]; - this.list = this.instantiationService.createInstance(WorkbenchList, `SCM Repo`, this.listContainer, delegate, renderers, { - identityProvider: scmResourceIdentityProvider, - keyboardNavigationLabelProvider: scmKeyboardNavigationLabelProvider, - horizontalScrolling: false - }); + const dataSource = new SCMTreeDataSource(); - this._register(Event.chain(this.list.onDidOpen) + this.tree = this.instantiationService.createInstance( + WorkbenchDataTree, + `SCM Tree Repo`, + this.listContainer, + delegate, + renderers, + dataSource, + { + identityProvider: scmResourceIdentityProvider, + keyboardNavigationLabelProvider: scmKeyboardNavigationLabelProvider, + horizontalScrolling: false + }) as WorkbenchDataTree; + + this._register(Event.chain(this.tree.onDidOpen) .map(e => e.elements[0]) - .filter(e => !!e && isSCMResource(e)) + .filter(e => !!e && typeof e !== 'string' && isSCMResource(e)) .on(this.open, this)); - this._register(Event.chain(this.list.onPin) - .map(e => e.elements[0]) - .filter(e => !!e && isSCMResource(e)) - .on(this.pin, this)); + // this._register(Event.chain(this.tree.onPin) + // .map(e => e.elements[0]) + // .filter(e => !!e && isSCMResource(e)) + // .on(this.pin, this)); - this._register(this.list.onContextMenu(this.onListContextMenu, this)); - this._register(this.list); + // this._register(this.tree.onContextMenu(this.onListContextMenu, this)); + this._register(this.tree); - this._register(this.viewModel.onDidChangeVisibility(this.onDidChangeVisibility, this)); - this.onDidChangeVisibility(this.viewModel.isVisible()); + this.tree.setInput(this.repository); + + // this._register(this.viewModel.onDidChangeVisibility(this.onDidChangeVisibility, this)); + // this.onDidChangeVisibility(this.viewModel.isVisible()); + this.onDidChangeVisibility(true); this.onDidChangeBodyVisibility(visible => this.inputBox.setEnabled(visible)); } private onDidChangeVisibility(visible: boolean): void { - if (visible) { - const listSplicer = new ResourceGroupSplicer(this.repository.provider.groups, this.list); - this.visibilityDisposables.push(listSplicer); - } else { - this.cachedScrollTop = this.list.scrollTop; - this.visibilityDisposables = dispose(this.visibilityDisposables); - } + // if (visible) { + const listSplicer = new ResourceGroupSplicer(this.repository.provider.groups, this.tree); + this.visibilityDisposables.push(listSplicer); + // } else { + // this.cachedScrollTop = this.tree.scrollTop; + // this.visibilityDisposables = dispose(this.visibilityDisposables); + // } } layoutBody(height: number | undefined = this.cachedHeight, width: number | undefined = this.cachedWidth): void { @@ -896,16 +940,16 @@ export class RepositoryPanel extends ViewletPanel { const editorHeight = this.inputBox.height; const listHeight = height - (editorHeight + 12 /* margin */); this.listContainer.style.height = `${listHeight}px`; - this.list.layout(listHeight, width); + this.tree.layout(listHeight, width); } else { addClass(this.inputBoxContainer, 'hidden'); this.listContainer.style.height = `${height}px`; - this.list.layout(height, width); + this.tree.layout(height, width); } - if (this.cachedScrollTop !== undefined && this.list.scrollTop !== this.cachedScrollTop) { - this.list.scrollTop = Math.min(this.cachedScrollTop, this.list.scrollHeight); + if (this.cachedScrollTop !== undefined && this.tree.scrollTop !== this.cachedScrollTop) { + this.tree.scrollTop = Math.min(this.cachedScrollTop, this.tree.scrollHeight); // Applying the cached scroll position just once until the next leave. // This, also, avoids the scrollbar to flicker when resizing the sidebar. this.cachedScrollTop = undefined; @@ -919,7 +963,7 @@ export class RepositoryPanel extends ViewletPanel { if (this.repository.input.visible) { this.inputBox.focus(); } else { - this.list.domFocus(); + this.tree.domFocus(); } this.repository.focus(); @@ -950,38 +994,38 @@ export class RepositoryPanel extends ViewletPanel { e.open(); } - private pin(): void { - const activeControl = this.editorService.activeControl; - if (activeControl) { - activeControl.group.pinEditor(activeControl.input); - } - } + // private pin(): void { + // const activeControl = this.editorService.activeControl; + // if (activeControl) { + // activeControl.group.pinEditor(activeControl.input); + // } + // } - private onListContextMenu(e: IListContextMenuEvent): void { - if (!e.element) { - return; - } + // private onListContextMenu(e: IListContextMenuEvent): void { + // if (!e.element) { + // return; + // } - const element = e.element; - let actions: IAction[]; + // const element = e.element; + // let actions: IAction[]; - if (isSCMResource(element)) { - actions = this.menus.getResourceContextActions(element); - } else { - actions = this.menus.getResourceGroupContextActions(element); - } + // if (isSCMResource(element)) { + // actions = this.menus.getResourceContextActions(element); + // } else { + // actions = this.menus.getResourceGroupContextActions(element); + // } - this.contextMenuService.showContextMenu({ - getAnchor: () => e.anchor, - getActions: () => actions, - getActionsContext: () => element, - actionRunner: new MultipleSelectionActionRunner(() => this.getSelectedResources()) - }); - } + // this.contextMenuService.showContextMenu({ + // getAnchor: () => e.anchor, + // getActions: () => actions, + // getActionsContext: () => element, + // actionRunner: new MultipleSelectionActionRunner(() => this.getSelectedResources()) + // }); + // } private getSelectedResources(): ISCMResource[] { - return this.list.getSelectedElements() - .filter(r => isSCMResource(r)) as ISCMResource[]; + return this.tree.getSelection() + .filter(r => !!r && (typeof r !== 'string') && isSCMResource(r)) as ISCMResource[]; } private updateInputBox(): void { From de310672aab72b4bff85fa5110f8569ec1eda9ce Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 11 Sep 2019 22:11:43 +0200 Subject: [PATCH 02/42] typo --- src/vs/workbench/contrib/scm/browser/scmUtil.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmUtil.ts b/src/vs/workbench/contrib/scm/browser/scmUtil.ts index 2214966835b..e77bd7ce8fb 100644 --- a/src/vs/workbench/contrib/scm/browser/scmUtil.ts +++ b/src/vs/workbench/contrib/scm/browser/scmUtil.ts @@ -9,7 +9,7 @@ export function isSCMRepository(element: ISCMRepository | ISCMResourceGroup | IS return !!(element as ISCMRepository).provider && typeof (element as ISCMRepository).setSelected === 'function'; } -export function isSCMResourceGroup(element: ISCMRepository | ISCMResourceGroup | ISCMResource): element is ISCMRepository { +export function isSCMResourceGroup(element: ISCMRepository | ISCMResourceGroup | ISCMResource): element is ISCMResourceGroup { return !!(element as ISCMResourceGroup).provider && !!(element as ISCMResourceGroup).elements; } From eeebde67dc686139d2a199c88c8b3e6da8e1e5f3 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 11 Sep 2019 22:11:57 +0200 Subject: [PATCH 03/42] wip: scm tree filter --- .../contrib/scm/browser/scmViewlet.ts | 69 ++++++++++++------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index 87866e09fc4..d1532c965d3 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -29,7 +29,7 @@ import { createAndFillInContextMenuActions, ContextAwareMenuEntryActionViewItem, import { SCMMenus } from './scmMenus'; import { ActionBar, IActionViewItemProvider, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService, LIGHT } from 'vs/platform/theme/common/themeService'; -import { isSCMResource, isSCMRepository } from './scmUtil'; +import { isSCMResource, isSCMRepository, isSCMResourceGroup } from './scmUtil'; import { attachBadgeStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; @@ -50,7 +50,7 @@ import { IViewsRegistry, IViewDescriptor, Extensions } from 'vs/workbench/common import { Registry } from 'vs/platform/registry/common/platform'; import { nextTick } from 'vs/base/common/process'; import { DataTree } from 'vs/base/browser/ui/tree/dataTree'; -import { ITreeRenderer, ITreeNode, IDataSource } from 'vs/base/browser/ui/tree/tree'; +import { ITreeRenderer, ITreeNode, IDataSource, ITreeFilter } from 'vs/base/browser/ui/tree/tree'; import { ISequence, ISpliceable, ISplice } from 'vs/base/common/sequence'; export interface ISpliceEvent { @@ -589,6 +589,19 @@ class SCMTreeDataSource implements IDataSource { } } +class SCMTreeFilter implements ITreeFilter { + + filter(element: TreeNode): boolean { + if (typeof element === 'string') { + return true; + } else if (isSCMResourceGroup(element)) { + return element.elements.length > 0 || !element.hideWhenEmpty; + } else { + return true; + } + } +} + const scmResourceIdentityProvider = new class implements IIdentityProvider { getId(r: ISCMResourceGroup | ISCMResource): string { if (isSCMResource(r)) { @@ -618,6 +631,7 @@ const scmKeyboardNavigationLabelProvider = new class implements IKeyboardNavigat interface IGroupItem { readonly group: ISCMResourceGroup; + visible: boolean; readonly disposable: IDisposable; } @@ -639,7 +653,7 @@ class ResourceGroupSplicer { for (const group of toInsert) { const disposable = combinedDisposable( - // group.onDidChange(() => this.onDidChangeGroup(group)), + group.onDidChange(() => this.onDidChangeGroup(group)), group.onDidSplice(splice => this.onDidSpliceGroup(group, splice)) ); @@ -655,35 +669,36 @@ class ResourceGroupSplicer { this.tree.updateChildren(); } - // private onDidChangeGroup(group: ISCMResourceGroup): void { - // const itemIndex = firstIndex(this.items, item => item.group === group); + private onDidChangeGroup(group: ISCMResourceGroup): void { + this.tree.updateChildren(); + // const itemIndex = firstIndex(this.items, item => item.group === group); - // if (itemIndex < 0) { - // return; - // } + // if (itemIndex < 0) { + // return; + // } - // const item = this.items[itemIndex]; - // const visible = isGroupVisible(group); + // const item = this.items[itemIndex]; + // const visible = isGroupVisible(group); - // if (item.visible === visible) { - // return; - // } + // if (item.visible === visible) { + // return; + // } - // let absoluteStart = 0; + // let absoluteStart = 0; - // for (let i = 0; i < itemIndex; i++) { - // const item = this.items[i]; - // absoluteStart += (item.visible ? 1 : 0) + item.group.elements.length; - // } + // for (let i = 0; i < itemIndex; i++) { + // const item = this.items[i]; + // absoluteStart += (item.visible ? 1 : 0) + item.group.elements.length; + // } - // if (visible) { - // this.spliceable.splice(absoluteStart, 0, [group, ...group.elements]); - // } else { - // this.spliceable.splice(absoluteStart, 1 + group.elements.length, []); - // } + // if (visible) { + // this.spliceable.splice(absoluteStart, 0, [group, ...group.elements]); + // } else { + // this.spliceable.splice(absoluteStart, 1 + group.elements.length, []); + // } - // item.visible = visible; - // } + // item.visible = visible; + } private onDidSpliceGroup(group: ISCMResourceGroup, { start, deleteCount, toInsert }: ISplice): void { this.tree.updateChildren(group); @@ -881,6 +896,7 @@ export class RepositoryPanel extends ViewletPanel { ]; const dataSource = new SCMTreeDataSource(); + const filter = new SCMTreeFilter(); this.tree = this.instantiationService.createInstance( WorkbenchDataTree, @@ -892,7 +908,8 @@ export class RepositoryPanel extends ViewletPanel { { identityProvider: scmResourceIdentityProvider, keyboardNavigationLabelProvider: scmKeyboardNavigationLabelProvider, - horizontalScrolling: false + horizontalScrolling: false, + filter }) as WorkbenchDataTree; this._register(Event.chain(this.tree.onDidOpen) From c6d524267ba3677d5d60d71c01d3bfe18eae188c Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 13 Sep 2019 12:31:31 +0200 Subject: [PATCH 04/42] scm: resource tree model --- src/vs/base/common/resourceTree.ts | 103 +++++++++++++++++++ src/vs/base/test/common/resourceTree.test.ts | 49 +++++++++ 2 files changed, 152 insertions(+) create mode 100644 src/vs/base/common/resourceTree.ts create mode 100644 src/vs/base/test/common/resourceTree.test.ts diff --git a/src/vs/base/common/resourceTree.ts b/src/vs/base/common/resourceTree.ts new file mode 100644 index 00000000000..c80ad0e97fa --- /dev/null +++ b/src/vs/base/common/resourceTree.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; + +export const enum NodeType { + Branch, + Leaf +} + +export interface LeafNode { + readonly type: NodeType.Leaf; + readonly element: T; +} + +export interface BranchNode { + readonly type: NodeType.Branch; + readonly children: Map>; +} + +export type Node = BranchNode | LeafNode; + +export class ResourceTree> { + + readonly root: BranchNode = { type: NodeType.Branch, children: new Map() }; + + constructor() { } + + add(uri: URI, element: T): void { + const parts = uri.fsPath.split(/[\\\/]/).filter(p => !!p); + let node = this.root; + + for (let i = 0; i < parts.length; i++) { + const name = parts[i]; + let child = node.children.get(name); + + if (!child) { + if (i < parts.length - 1) { + child = { type: NodeType.Branch, children: new Map() }; + node.children.set(name, child); + } else { + child = { type: NodeType.Leaf, element }; + node.children.set(name, child); + return; + } + } + + if (child.type === NodeType.Leaf) { + if (i < parts.length - 1) { + throw new Error('Inconsistent tree: can\'t override leaf with branch.'); + } + + // replace + node.children.set(name, { type: NodeType.Leaf, element }); + return; + } else if (i === parts.length - 1) { + throw new Error('Inconsistent tree: can\'t override branch with leaf.'); + } + + node = child; + } + } + + delete(uri: URI): T | undefined { + const parts = uri.fsPath.split(/[\\\/]/).filter(p => !!p); + return this._delete(this.root, parts, 0); + } + + private _delete(node: BranchNode, parts: string[], index: number): T | undefined { + const name = parts[index]; + const child = node.children.get(name); + + if (!child) { + return undefined; + } + + // not at end + if (index < parts.length - 1) { + if (child.type === NodeType.Leaf) { + throw new Error('Inconsistent tree: Expected a branch, found a leaf instead.'); + } else { + const result = this._delete(child, parts, index + 1); + + if (typeof result !== 'undefined' && child.children.size === 0) { + node.children.delete(name); + } + + return result; + } + } + + //at end + if (child.type === NodeType.Branch) { + // TODO: maybe we can allow this + throw new Error('Inconsistent tree: Expected a leaf, found a branch instead.'); + } + + node.children.delete(name); + return child.element; + } +} diff --git a/src/vs/base/test/common/resourceTree.test.ts b/src/vs/base/test/common/resourceTree.test.ts new file mode 100644 index 00000000000..1ac3fd8904f --- /dev/null +++ b/src/vs/base/test/common/resourceTree.test.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { ResourceTree, NodeType, BranchNode, LeafNode } from 'vs/base/common/resourceTree'; +import { URI } from 'vs/base/common/uri'; + +suite('ResourceTree', function () { + test('ctor', function () { + const tree = new ResourceTree(); + assert.equal(tree.root.type, NodeType.Branch); + assert.equal(tree.root.children.size, 0); + }); + + test('simple', function () { + const tree = new ResourceTree(); + + tree.add(URI.file('/foo/bar.txt'), 'bar contents'); + assert.equal(tree.root.type, NodeType.Branch); + assert.equal(tree.root.children.size, 1); + + let foo = tree.root.children.get('foo') as BranchNode; + assert(foo); + assert.equal(foo.type, NodeType.Branch); + assert.equal(foo.children.size, 1); + + let bar = foo.children.get('bar.txt') as LeafNode; + assert(bar); + assert.equal(bar.type, NodeType.Leaf); + assert.equal(bar.element, 'bar contents'); + + tree.add(URI.file('/hello.txt'), 'hello contents'); + assert.equal(tree.root.children.size, 2); + + let hello = tree.root.children.get('hello.txt') as LeafNode; + assert(hello); + assert.equal(hello.type, NodeType.Leaf); + assert.equal(hello.element, 'hello contents'); + + tree.delete(URI.file('/foo/bar.txt')); + assert.equal(tree.root.children.size, 1); + hello = tree.root.children.get('hello.txt') as LeafNode; + assert(hello); + assert.equal(hello.type, NodeType.Leaf); + assert.equal(hello.element, 'hello contents'); + }); +}); From 1d3ae86113593ca0062456bab959bbf2f6ffbd8e Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 13 Sep 2019 16:05:12 +0200 Subject: [PATCH 05/42] Iterator.fromIterableIterator --- src/vs/base/common/iterator.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index db53b8aef57..d274fbbd5d3 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -56,6 +56,20 @@ export module Iterator { }; } + export function fromIterableIterator(it: IterableIterator): Iterator { + return { + next(): IteratorResult { + const result = it.next(); + + if (result.done) { + return FIN; + } + + return { done: false, value: result.value }; + } + }; + } + export function from(elements: Iterator | T[] | undefined): Iterator { if (!elements) { return Iterator.empty(); From 8e0dbd5f3a67345731fa35c85ad2964a19c85082 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 13 Sep 2019 16:05:23 +0200 Subject: [PATCH 06/42] finish resource tree --- src/vs/base/common/resourceTree.ts | 105 ++++++++++++++----- src/vs/base/test/common/resourceTree.test.ts | 32 +++--- 2 files changed, 94 insertions(+), 43 deletions(-) diff --git a/src/vs/base/common/resourceTree.ts b/src/vs/base/common/resourceTree.ts index c80ad0e97fa..93346bf26a8 100644 --- a/src/vs/base/common/resourceTree.ts +++ b/src/vs/base/common/resourceTree.ts @@ -4,56 +4,107 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; +import { memoize } from 'vs/base/common/decorators'; +import * as paths from 'vs/base/common/path'; +import { Iterator } from 'vs/base/common/iterator'; -export const enum NodeType { - Branch, - Leaf -} - -export interface LeafNode { - readonly type: NodeType.Leaf; +export interface ILeafNode { + readonly path: string; + readonly name: string; readonly element: T; } -export interface BranchNode { - readonly type: NodeType.Branch; - readonly children: Map>; +export interface IBranchNode { + readonly path: string; + readonly name: string; + readonly size: number; + readonly children: Iterator>; + get(childName: string): INode | undefined; } -export type Node = BranchNode | LeafNode; +export type INode = IBranchNode | ILeafNode; + +export function isBranchNode(obj: any): obj is IBranchNode { + return obj instanceof BranchNode; +} + +// Internals + +class Node { + + @memoize + get name(): string { return paths.posix.basename(this.path); } + + constructor(readonly path: string) { } +} + +class BranchNode extends Node implements IBranchNode { + + private _children = new Map | LeafNode>(); + + get size(): number { + return this._children.size; + } + + get children(): Iterator | LeafNode> { + return Iterator.fromIterableIterator(this._children.values()); + } + + get(path: string): BranchNode | LeafNode | undefined { + return this._children.get(path); + } + + set(path: string, child: BranchNode | LeafNode): void { + this._children.set(path, child); + } + + delete(path: string): void { + this._children.delete(path); + } +} + +class LeafNode extends Node implements ILeafNode { + + constructor(path: string, readonly element: T) { + super(path); + } +} export class ResourceTree> { - readonly root: BranchNode = { type: NodeType.Branch, children: new Map() }; + readonly root = new BranchNode(''); constructor() { } add(uri: URI, element: T): void { const parts = uri.fsPath.split(/[\\\/]/).filter(p => !!p); let node = this.root; + let path = this.root.path; for (let i = 0; i < parts.length; i++) { const name = parts[i]; - let child = node.children.get(name); + path = path + '/' + name; + + let child = node.get(name); if (!child) { if (i < parts.length - 1) { - child = { type: NodeType.Branch, children: new Map() }; - node.children.set(name, child); + child = new BranchNode(path); + node.set(name, child); } else { - child = { type: NodeType.Leaf, element }; - node.children.set(name, child); + child = new LeafNode(path, element); + node.set(name, child); return; } } - if (child.type === NodeType.Leaf) { + if (!(child instanceof BranchNode)) { if (i < parts.length - 1) { throw new Error('Inconsistent tree: can\'t override leaf with branch.'); } // replace - node.children.set(name, { type: NodeType.Leaf, element }); + node.set(name, new LeafNode(path, element)); return; } else if (i === parts.length - 1) { throw new Error('Inconsistent tree: can\'t override branch with leaf.'); @@ -70,7 +121,7 @@ export class ResourceTree> { private _delete(node: BranchNode, parts: string[], index: number): T | undefined { const name = parts[index]; - const child = node.children.get(name); + const child = node.get(name); if (!child) { return undefined; @@ -78,26 +129,26 @@ export class ResourceTree> { // not at end if (index < parts.length - 1) { - if (child.type === NodeType.Leaf) { - throw new Error('Inconsistent tree: Expected a branch, found a leaf instead.'); - } else { + if (child instanceof BranchNode) { const result = this._delete(child, parts, index + 1); - if (typeof result !== 'undefined' && child.children.size === 0) { - node.children.delete(name); + if (typeof result !== 'undefined' && child.size === 0) { + node.delete(name); } return result; + } else { + throw new Error('Inconsistent tree: Expected a branch, found a leaf instead.'); } } //at end - if (child.type === NodeType.Branch) { + if (child instanceof BranchNode) { // TODO: maybe we can allow this throw new Error('Inconsistent tree: Expected a leaf, found a branch instead.'); } - node.children.delete(name); + node.delete(name); return child.element; } } diff --git a/src/vs/base/test/common/resourceTree.test.ts b/src/vs/base/test/common/resourceTree.test.ts index 1ac3fd8904f..ad1797ca875 100644 --- a/src/vs/base/test/common/resourceTree.test.ts +++ b/src/vs/base/test/common/resourceTree.test.ts @@ -4,46 +4,46 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ResourceTree, NodeType, BranchNode, LeafNode } from 'vs/base/common/resourceTree'; +import { ResourceTree, IBranchNode, ILeafNode, isBranchNode } from 'vs/base/common/resourceTree'; import { URI } from 'vs/base/common/uri'; suite('ResourceTree', function () { test('ctor', function () { const tree = new ResourceTree(); - assert.equal(tree.root.type, NodeType.Branch); - assert.equal(tree.root.children.size, 0); + assert(isBranchNode(tree.root)); + assert.equal(tree.root.size, 0); }); test('simple', function () { const tree = new ResourceTree(); tree.add(URI.file('/foo/bar.txt'), 'bar contents'); - assert.equal(tree.root.type, NodeType.Branch); - assert.equal(tree.root.children.size, 1); + assert(isBranchNode(tree.root)); + assert.equal(tree.root.size, 1); - let foo = tree.root.children.get('foo') as BranchNode; + let foo = tree.root.get('foo') as IBranchNode; assert(foo); - assert.equal(foo.type, NodeType.Branch); - assert.equal(foo.children.size, 1); + assert(isBranchNode(foo)); + assert.equal(foo.size, 1); - let bar = foo.children.get('bar.txt') as LeafNode; + let bar = foo.get('bar.txt') as ILeafNode; assert(bar); - assert.equal(bar.type, NodeType.Leaf); + assert(!isBranchNode(bar)); assert.equal(bar.element, 'bar contents'); tree.add(URI.file('/hello.txt'), 'hello contents'); - assert.equal(tree.root.children.size, 2); + assert.equal(tree.root.size, 2); - let hello = tree.root.children.get('hello.txt') as LeafNode; + let hello = tree.root.get('hello.txt') as ILeafNode; assert(hello); - assert.equal(hello.type, NodeType.Leaf); + assert(!isBranchNode(hello)); assert.equal(hello.element, 'hello contents'); tree.delete(URI.file('/foo/bar.txt')); - assert.equal(tree.root.children.size, 1); - hello = tree.root.children.get('hello.txt') as LeafNode; + assert.equal(tree.root.size, 1); + hello = tree.root.get('hello.txt') as ILeafNode; assert(hello); - assert.equal(hello.type, NodeType.Leaf); + assert(!isBranchNode(hello)); assert.equal(hello.element, 'hello contents'); }); }); From d9c0f275c0e5191335173a55925f3e85a256139c Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 13 Sep 2019 16:05:33 +0200 Subject: [PATCH 07/42] wip: scm as tree --- .../contrib/scm/browser/scmViewlet.ts | 146 ++++++++++-------- 1 file changed, 85 insertions(+), 61 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index d1532c965d3..c17c2e29908 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -29,7 +29,7 @@ import { createAndFillInContextMenuActions, ContextAwareMenuEntryActionViewItem, import { SCMMenus } from './scmMenus'; import { ActionBar, IActionViewItemProvider, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService, LIGHT } from 'vs/platform/theme/common/themeService'; -import { isSCMResource, isSCMRepository, isSCMResourceGroup } from './scmUtil'; +import { isSCMResource, isSCMResourceGroup } from './scmUtil'; import { attachBadgeStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; @@ -37,7 +37,7 @@ import { Command } from 'vs/editor/common/modes'; import { renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { format } from 'vs/base/common/strings'; import { equals } from 'vs/base/common/arrays'; -import { WorkbenchList, WorkbenchDataTree } from 'vs/platform/list/browser/listService'; +import { WorkbenchList, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ThrottledDelayer } from 'vs/base/common/async'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -49,9 +49,12 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IViewsRegistry, IViewDescriptor, Extensions } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { nextTick } from 'vs/base/common/process'; -import { DataTree } from 'vs/base/browser/ui/tree/dataTree'; -import { ITreeRenderer, ITreeNode, IDataSource, ITreeFilter } from 'vs/base/browser/ui/tree/tree'; -import { ISequence, ISpliceable, ISplice } from 'vs/base/common/sequence'; +import { ITreeRenderer, ITreeNode, ITreeFilter, ITreeElement } from 'vs/base/browser/ui/tree/tree'; +import { ISequence, ISplice } from 'vs/base/common/sequence'; +import { ResourceTree, IBranchNode, isBranchNode, INode } from 'vs/base/common/resourceTree'; +import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; +import { Iterator } from 'vs/base/common/iterator'; +import * as paths from 'vs/base/common/path'; export interface ISpliceEvent { index: number; @@ -431,7 +434,7 @@ interface FolderTemplate { name: HTMLElement; } -class FolderRenderer implements ITreeRenderer { +class FolderRenderer implements ITreeRenderer, void, FolderTemplate> { static TEMPLATE_ID = 'folder'; get templateId(): string { return FolderRenderer.TEMPLATE_ID; } @@ -443,8 +446,8 @@ class FolderRenderer implements ITreeRenderer { return { name }; } - renderElement(node: ITreeNode, index: number, template: FolderTemplate): void { - template.name.textContent = node.element; + renderElement(node: ITreeNode>, index: number, template: FolderTemplate): void { + template.name.textContent = node.element.name; } disposeTemplate(): void { @@ -557,42 +560,25 @@ class ResourceRenderer implements ITreeRenderer { +class ProviderListDelegate implements IListVirtualDelegate { getHeight() { return 22; } - getTemplateId(element: TreeNode) { - if (typeof element === 'string') { + getTemplateId(element: TreeElement) { + if (isBranchNode(element)) { return FolderRenderer.TEMPLATE_ID; - } - - return isSCMResource(element) ? ResourceRenderer.TEMPLATE_ID : ResourceGroupRenderer.TEMPLATE_ID; - } -} - -class SCMTreeDataSource implements IDataSource { - - hasChildren(element: ISCMRepository | TreeNode): boolean { - return typeof element !== 'string' && !isSCMResource(element); - } - - getChildren(element: ISCMRepository | TreeNode): TreeNode[] { - if (typeof element === 'string') { - return []; - } else if (isSCMRepository(element)) { - return element.provider.groups.elements; } else if (isSCMResource(element)) { - return []; - } else { // ISCMResourceGroup - return element.elements; + return ResourceRenderer.TEMPLATE_ID; + } else { + return ResourceGroupRenderer.TEMPLATE_ID; } } } -class SCMTreeFilter implements ITreeFilter { +class SCMTreeFilter implements ITreeFilter { - filter(element: TreeNode): boolean { - if (typeof element === 'string') { + filter(element: TreeElement): boolean { + if (isBranchNode(element)) { return true; } else if (isSCMResourceGroup(element)) { return element.elements.length > 0 || !element.hideWhenEmpty; @@ -602,22 +588,26 @@ class SCMTreeFilter implements ITreeFilter { } } -const scmResourceIdentityProvider = new class implements IIdentityProvider { - getId(r: ISCMResourceGroup | ISCMResource): string { - if (isSCMResource(r)) { - const group = r.resourceGroup; +const scmResourceIdentityProvider = new class implements IIdentityProvider { + getId(e: TreeElement): string { + if (isBranchNode(e)) { + return e.path; + } else if (isSCMResource(e)) { + const group = e.resourceGroup; const provider = group.provider; - return `${provider.contextValue}/${group.id}/${r.sourceUri.toString()}`; + return `${provider.contextValue}/${group.id}/${e.sourceUri.toString()}`; } else { - const provider = r.provider; - return `${provider.contextValue}/${r.id}`; + const provider = e.provider; + return `${provider.contextValue}/${e.id}`; } } }; -const scmKeyboardNavigationLabelProvider = new class implements IKeyboardNavigationLabelProvider { - getKeyboardNavigationLabel(e: ISCMResourceGroup | ISCMResource) { - if (isSCMResource(e)) { +const scmKeyboardNavigationLabelProvider = new class implements IKeyboardNavigationLabelProvider { + getKeyboardNavigationLabel(e: TreeElement) { + if (isBranchNode(e)) { + return paths.posix.basename(e.path); + } else if (isSCMResource(e)) { return basename(e.sourceUri); } else { return e.label; @@ -631,10 +621,24 @@ const scmKeyboardNavigationLabelProvider = new class implements IKeyboardNavigat interface IGroupItem { readonly group: ISCMResourceGroup; - visible: boolean; + readonly resources: ISCMResource[]; + readonly tree: ResourceTree; + // visible: boolean; readonly disposable: IDisposable; } +function asTreeElement(node: INode): ITreeElement { + if (isBranchNode(node)) { + return { + element: node, + children: Iterator.map(node.children, asTreeElement), + collapsed: false + }; + } + + return { element: node.element }; +} + class ResourceGroupSplicer { private items: IGroupItem[] = []; @@ -642,22 +646,34 @@ class ResourceGroupSplicer { constructor( groupSequence: ISequence, - private tree: DataTree + private tree: ObjectTree ) { groupSequence.onDidSplice(this.onDidSpliceGroups, this, this.disposables); this.onDidSpliceGroups({ start: 0, deleteCount: 0, toInsert: groupSequence.elements }); } + private fullRefresh(): void { + this.tree.setChildren(null, this.items.map(item => { + return { + element: item.group, + children: Iterator.map(item.tree.root.children, asTreeElement) + }; + })); + } + private onDidSpliceGroups({ start, deleteCount, toInsert }: ISplice): void { const itemsToInsert: IGroupItem[] = []; for (const group of toInsert) { + const tree = new ResourceTree(); + const resources: ISCMResource[] = [...group.elements]; const disposable = combinedDisposable( group.onDidChange(() => this.onDidChangeGroup(group)), - group.onDidSplice(splice => this.onDidSpliceGroup(group, splice)) + group.onDidSplice(splice => this.onDidSpliceGroup(item, splice)) ); + const item = { group, resources, tree, disposable }; - itemsToInsert.push({ group, disposable }); + itemsToInsert.push(item); } const itemsToDispose = this.items.splice(start, deleteCount, ...itemsToInsert); @@ -666,11 +682,11 @@ class ResourceGroupSplicer { item.disposable.dispose(); } - this.tree.updateChildren(); + this.fullRefresh(); } private onDidChangeGroup(group: ISCMResourceGroup): void { - this.tree.updateChildren(); + this.fullRefresh(); // const itemIndex = firstIndex(this.items, item => item.group === group); // if (itemIndex < 0) { @@ -700,8 +716,18 @@ class ResourceGroupSplicer { // item.visible = visible; } - private onDidSpliceGroup(group: ISCMResourceGroup, { start, deleteCount, toInsert }: ISplice): void { - this.tree.updateChildren(group); + private onDidSpliceGroup(item: IGroupItem, { start, deleteCount, toInsert }: ISplice): void { + for (const resource of toInsert) { + item.tree.add(resource.sourceUri, resource); + } + + const deleted = item.resources.splice(start, deleteCount, ...toInsert); + + for (const resource of deleted) { + item.tree.delete(resource.sourceUri); + } + + this.fullRefresh(); // const itemIndex = firstIndex(this.items, item => item.group === group); // if (itemIndex < 0) { @@ -749,7 +775,7 @@ function convertValidationType(type: InputValidationType): MessageType { } } -type TreeNode = ISCMResourceGroup | string | ISCMResource; +type TreeElement = ISCMResourceGroup | IBranchNode | ISCMResource; export class RepositoryPanel extends ViewletPanel { @@ -759,7 +785,7 @@ export class RepositoryPanel extends ViewletPanel { private inputBoxContainer: HTMLElement; private inputBox: InputBox; private listContainer: HTMLElement; - private tree: DataTree; + private tree: ObjectTree; private listLabels: ResourceLabels; private menus: SCMMenus; private visibilityDisposables: IDisposable[] = []; @@ -895,26 +921,24 @@ export class RepositoryPanel extends ViewletPanel { new ResourceRenderer(this.listLabels, actionViewItemProvider, () => this.getSelectedResources(), this.themeService, this.menus) ]; - const dataSource = new SCMTreeDataSource(); const filter = new SCMTreeFilter(); this.tree = this.instantiationService.createInstance( - WorkbenchDataTree, + WorkbenchObjectTree, `SCM Tree Repo`, this.listContainer, delegate, renderers, - dataSource, { identityProvider: scmResourceIdentityProvider, keyboardNavigationLabelProvider: scmKeyboardNavigationLabelProvider, horizontalScrolling: false, filter - }) as WorkbenchDataTree; + }); this._register(Event.chain(this.tree.onDidOpen) .map(e => e.elements[0]) - .filter(e => !!e && typeof e !== 'string' && isSCMResource(e)) + .filter(e => !!e && !isBranchNode(e) && isSCMResource(e)) .on(this.open, this)); // this._register(Event.chain(this.tree.onPin) @@ -925,7 +949,7 @@ export class RepositoryPanel extends ViewletPanel { // this._register(this.tree.onContextMenu(this.onListContextMenu, this)); this._register(this.tree); - this.tree.setInput(this.repository); + // this.tree.setInput(this.repository); // this._register(this.viewModel.onDidChangeVisibility(this.onDidChangeVisibility, this)); // this.onDidChangeVisibility(this.viewModel.isVisible()); @@ -1042,7 +1066,7 @@ export class RepositoryPanel extends ViewletPanel { private getSelectedResources(): ISCMResource[] { return this.tree.getSelection() - .filter(r => !!r && (typeof r !== 'string') && isSCMResource(r)) as ISCMResource[]; + .filter(r => !!r && !isBranchNode(r) && isSCMResource(r)) as ISCMResource[]; } private updateInputBox(): void { From 3fea13f86fe78306a9dbf675c1cf941c354148de Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 13 Sep 2019 16:18:02 +0200 Subject: [PATCH 08/42] WorkbenchCompressibleObjectTree --- src/vs/platform/list/browser/listService.ts | 31 +++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 73ed8295cc0..65037724705 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -25,7 +25,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { attachListStyler, computeStyles, defaultListStyles } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; -import { ObjectTree, IObjectTreeOptions, ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; +import { ObjectTree, IObjectTreeOptions, ICompressibleTreeRenderer, CompressibleObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { ITreeEvent, ITreeRenderer, IAsyncDataSource, IDataSource, ITreeMouseEvent } from 'vs/base/browser/ui/tree/tree'; import { AsyncDataTree, IAsyncDataTreeOptions, CompressibleAsyncDataTree, ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; import { DataTree, IDataTreeOptions } from 'vs/base/browser/ui/tree/dataTree'; @@ -807,6 +807,33 @@ export class WorkbenchObjectTree, TFilterData = void> } } +export class WorkbenchCompressibleObjectTree, TFilterData = void> extends CompressibleObjectTree { + + private internals: WorkbenchTreeInternals; + get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; } + get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; } + + constructor( + user: string, + container: HTMLElement, + delegate: IListVirtualDelegate, + renderers: ICompressibleTreeRenderer[], + options: IObjectTreeOptions, + @IContextKeyService contextKeyService: IContextKeyService, + @IListService listService: IListService, + @IThemeService themeService: IThemeService, + @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService, + @IAccessibilityService accessibilityService: IAccessibilityService + ) { + const { options: treeOptions, getAutomaticKeyboardNavigation, disposable } = workbenchTreeDataPreamble(container, options, contextKeyService, themeService, configurationService, keybindingService, accessibilityService); + super(user, container, delegate, renderers, treeOptions); + this.disposables.push(disposable); + this.internals = new WorkbenchTreeInternals(this, treeOptions, getAutomaticKeyboardNavigation, contextKeyService, listService, themeService, configurationService, accessibilityService); + this.disposables.push(this.internals); + } +} + export class WorkbenchDataTree extends DataTree { private internals: WorkbenchTreeInternals; @@ -957,7 +984,7 @@ class WorkbenchTreeInternals { private disposables: IDisposable[] = []; constructor( - tree: WorkbenchObjectTree | WorkbenchDataTree | WorkbenchAsyncDataTree | WorkbenchCompressibleAsyncDataTree, + tree: WorkbenchObjectTree | CompressibleObjectTree | WorkbenchDataTree | WorkbenchAsyncDataTree | WorkbenchCompressibleAsyncDataTree, options: IAbstractTreeOptions | IAsyncDataTreeOptions, getAutomaticKeyboardNavigation: () => boolean | undefined, @IContextKeyService contextKeyService: IContextKeyService, From 47e686464124d059f9a2e4e79552bcb1636e9d12 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 13 Sep 2019 16:18:53 +0200 Subject: [PATCH 09/42] scm: adopt compressible tree --- .../contrib/scm/browser/scmViewlet.ts | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index c17c2e29908..1fc919ebbe3 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -37,7 +37,7 @@ import { Command } from 'vs/editor/common/modes'; import { renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { format } from 'vs/base/common/strings'; import { equals } from 'vs/base/common/arrays'; -import { WorkbenchList, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; +import { WorkbenchList, WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ThrottledDelayer } from 'vs/base/common/async'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -49,12 +49,13 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IViewsRegistry, IViewDescriptor, Extensions } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { nextTick } from 'vs/base/common/process'; -import { ITreeRenderer, ITreeNode, ITreeFilter, ITreeElement } from 'vs/base/browser/ui/tree/tree'; +import { ITreeNode, ITreeFilter } from 'vs/base/browser/ui/tree/tree'; import { ISequence, ISplice } from 'vs/base/common/sequence'; import { ResourceTree, IBranchNode, isBranchNode, INode } from 'vs/base/common/resourceTree'; -import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; +import { ObjectTree, ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { Iterator } from 'vs/base/common/iterator'; import * as paths from 'vs/base/common/path'; +import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; export interface ISpliceEvent { index: number; @@ -374,7 +375,7 @@ interface ResourceGroupTemplate { dispose: () => void; } -class ResourceGroupRenderer implements ITreeRenderer { +class ResourceGroupRenderer implements ICompressibleTreeRenderer { static TEMPLATE_ID = 'resource group'; get templateId(): string { return ResourceGroupRenderer.TEMPLATE_ID; } @@ -421,6 +422,10 @@ class ResourceGroupRenderer implements ITreeRenderer, void>, index: number, templateData: ResourceGroupTemplate, height: number | undefined): void { + throw new Error('Should never happen since node is incompressible'); + } + disposeElement(group: ITreeNode, index: number, template: ResourceGroupTemplate): void { template.elementDisposable.dispose(); } @@ -434,7 +439,7 @@ interface FolderTemplate { name: HTMLElement; } -class FolderRenderer implements ITreeRenderer, void, FolderTemplate> { +class FolderRenderer implements ICompressibleTreeRenderer, void, FolderTemplate> { static TEMPLATE_ID = 'folder'; get templateId(): string { return FolderRenderer.TEMPLATE_ID; } @@ -446,8 +451,12 @@ class FolderRenderer implements ITreeRenderer, void, F return { name }; } - renderElement(node: ITreeNode>, index: number, template: FolderTemplate): void { - template.name.textContent = node.element.name; + renderElement(node: ITreeNode>, index: number, templateData: FolderTemplate): void { + templateData.name.textContent = node.element.name; + } + + renderCompressedElements(node: ITreeNode>, void>, index: number, templateData: FolderTemplate, height: number | undefined): void { + templateData.name.textContent = node.element.elements.map(e => e.name).join('/'); } disposeTemplate(): void { @@ -487,7 +496,7 @@ class MultipleSelectionActionRunner extends ActionRunner { } } -class ResourceRenderer implements ITreeRenderer { +class ResourceRenderer implements ICompressibleTreeRenderer { static TEMPLATE_ID = 'resource'; get templateId(): string { return ResourceRenderer.TEMPLATE_ID; } @@ -550,6 +559,10 @@ class ResourceRenderer implements ITreeRenderer, void>, index: number, templateData: ResourceTemplate, height: number | undefined): void { + throw new Error('Should never happen since node is incompressible'); + } + disposeElement(resource: ITreeNode, index: number, template: ResourceTemplate): void { template.elementDisposable.dispose(); } @@ -627,16 +640,17 @@ interface IGroupItem { readonly disposable: IDisposable; } -function asTreeElement(node: INode): ITreeElement { +function asTreeElement(node: INode, incompressible: boolean): ICompressedTreeElement { if (isBranchNode(node)) { return { element: node, - children: Iterator.map(node.children, asTreeElement), + children: Iterator.map(node.children, node => asTreeElement(node, false)), + incompressible, collapsed: false }; } - return { element: node.element }; + return { element: node.element, incompressible: true }; } class ResourceGroupSplicer { @@ -656,8 +670,9 @@ class ResourceGroupSplicer { this.tree.setChildren(null, this.items.map(item => { return { element: item.group, - children: Iterator.map(item.tree.root.children, asTreeElement) - }; + children: Iterator.map(item.tree.root.children, node => asTreeElement(node, true)), + incompressible: true + } as ICompressedTreeElement; })); } @@ -924,7 +939,7 @@ export class RepositoryPanel extends ViewletPanel { const filter = new SCMTreeFilter(); this.tree = this.instantiationService.createInstance( - WorkbenchObjectTree, + WorkbenchCompressibleObjectTree, `SCM Tree Repo`, this.listContainer, delegate, From 7f159d00712fbd92398a8cb7b46856d1dde43243 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 13 Sep 2019 16:42:11 +0200 Subject: [PATCH 10/42] disable path in resource labels --- src/vs/workbench/contrib/scm/browser/scmViewlet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index 1fc919ebbe3..66f466fadef 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -536,7 +536,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer Date: Fri, 13 Sep 2019 16:53:56 +0200 Subject: [PATCH 11/42] remove URI from resource tree --- src/vs/base/common/resourceTree.ts | 11 ++++------- src/vs/base/test/common/resourceTree.test.ts | 7 +++---- src/vs/workbench/contrib/scm/browser/scmViewlet.ts | 9 ++++++--- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/vs/base/common/resourceTree.ts b/src/vs/base/common/resourceTree.ts index 93346bf26a8..79ea1f16c2c 100644 --- a/src/vs/base/common/resourceTree.ts +++ b/src/vs/base/common/resourceTree.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; import { memoize } from 'vs/base/common/decorators'; import * as paths from 'vs/base/common/path'; import { Iterator } from 'vs/base/common/iterator'; @@ -74,10 +73,8 @@ export class ResourceTree> { readonly root = new BranchNode(''); - constructor() { } - - add(uri: URI, element: T): void { - const parts = uri.fsPath.split(/[\\\/]/).filter(p => !!p); + add(key: string, element: T): void { + const parts = key.split(/[\\\/]/).filter(p => !!p); let node = this.root; let path = this.root.path; @@ -114,8 +111,8 @@ export class ResourceTree> { } } - delete(uri: URI): T | undefined { - const parts = uri.fsPath.split(/[\\\/]/).filter(p => !!p); + delete(key: string): T | undefined { + const parts = key.split(/[\\\/]/).filter(p => !!p); return this._delete(this.root, parts, 0); } diff --git a/src/vs/base/test/common/resourceTree.test.ts b/src/vs/base/test/common/resourceTree.test.ts index ad1797ca875..97af22d260b 100644 --- a/src/vs/base/test/common/resourceTree.test.ts +++ b/src/vs/base/test/common/resourceTree.test.ts @@ -5,7 +5,6 @@ import * as assert from 'assert'; import { ResourceTree, IBranchNode, ILeafNode, isBranchNode } from 'vs/base/common/resourceTree'; -import { URI } from 'vs/base/common/uri'; suite('ResourceTree', function () { test('ctor', function () { @@ -17,7 +16,7 @@ suite('ResourceTree', function () { test('simple', function () { const tree = new ResourceTree(); - tree.add(URI.file('/foo/bar.txt'), 'bar contents'); + tree.add('/foo/bar.txt', 'bar contents'); assert(isBranchNode(tree.root)); assert.equal(tree.root.size, 1); @@ -31,7 +30,7 @@ suite('ResourceTree', function () { assert(!isBranchNode(bar)); assert.equal(bar.element, 'bar contents'); - tree.add(URI.file('/hello.txt'), 'hello contents'); + tree.add('/hello.txt', 'hello contents'); assert.equal(tree.root.size, 2); let hello = tree.root.get('hello.txt') as ILeafNode; @@ -39,7 +38,7 @@ suite('ResourceTree', function () { assert(!isBranchNode(hello)); assert.equal(hello.element, 'hello contents'); - tree.delete(URI.file('/foo/bar.txt')); + tree.delete('/foo/bar.txt'); assert.equal(tree.root.size, 1); hello = tree.root.get('hello.txt') as ILeafNode; assert(hello); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index 66f466fadef..67df43fdb2f 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/scmViewlet'; import { localize } from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; -import { basename } from 'vs/base/common/resources'; +import { basename, relativePath } from 'vs/base/common/resources'; import { IDisposable, dispose, Disposable, DisposableStore, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; import { append, $, addClass, toggleClass, trackFocus, removeClass, addClasses } from 'vs/base/browser/dom'; @@ -56,6 +56,7 @@ import { ObjectTree, ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/o import { Iterator } from 'vs/base/common/iterator'; import * as paths from 'vs/base/common/path'; import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { URI } from 'vs/base/common/uri'; export interface ISpliceEvent { index: number; @@ -732,14 +733,16 @@ class ResourceGroupSplicer { } private onDidSpliceGroup(item: IGroupItem, { start, deleteCount, toInsert }: ISplice): void { + const root = item.group.provider.rootUri || URI.file('/'); + for (const resource of toInsert) { - item.tree.add(resource.sourceUri, resource); + item.tree.add(relativePath(root, resource.sourceUri) || resource.sourceUri.fsPath, resource); } const deleted = item.resources.splice(start, deleteCount, ...toInsert); for (const resource of deleted) { - item.tree.delete(resource.sourceUri); + item.tree.delete(relativePath(root, resource.sourceUri) || resource.sourceUri.fsPath); } this.fullRefresh(); From 9db2dd5b869029d9319e24db29371bf5e372bf2a Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 17 Sep 2019 10:29:47 +0200 Subject: [PATCH 12/42] scm: proper folder resources --- .../workbench/contrib/scm/browser/scmUtil.ts | 8 +- .../contrib/scm/browser/scmViewlet.ts | 114 ++++++++++-------- 2 files changed, 70 insertions(+), 52 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmUtil.ts b/src/vs/workbench/contrib/scm/browser/scmUtil.ts index e77bd7ce8fb..ec64c295f21 100644 --- a/src/vs/workbench/contrib/scm/browser/scmUtil.ts +++ b/src/vs/workbench/contrib/scm/browser/scmUtil.ts @@ -5,14 +5,14 @@ import { ISCMResource, ISCMRepository, ISCMResourceGroup } from 'vs/workbench/contrib/scm/common/scm'; -export function isSCMRepository(element: ISCMRepository | ISCMResourceGroup | ISCMResource): element is ISCMRepository { +export function isSCMRepository(element: any): element is ISCMRepository { return !!(element as ISCMRepository).provider && typeof (element as ISCMRepository).setSelected === 'function'; } -export function isSCMResourceGroup(element: ISCMRepository | ISCMResourceGroup | ISCMResource): element is ISCMResourceGroup { +export function isSCMResourceGroup(element: any): element is ISCMResourceGroup { return !!(element as ISCMResourceGroup).provider && !!(element as ISCMResourceGroup).elements; } -export function isSCMResource(element: ISCMRepository | ISCMResourceGroup | ISCMResource): element is ISCMResource { - return !!(element as ISCMResource).sourceUri; +export function isSCMResource(element: any): element is ISCMResource { + return !!(element as ISCMResource).sourceUri && isSCMResourceGroup((element as ISCMResource).resourceGroup); } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index 67df43fdb2f..b8a9965b357 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -49,14 +49,16 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IViewsRegistry, IViewDescriptor, Extensions } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { nextTick } from 'vs/base/common/process'; -import { ITreeNode, ITreeFilter } from 'vs/base/browser/ui/tree/tree'; +import { ITreeNode, ITreeFilter, ITreeSorter } from 'vs/base/browser/ui/tree/tree'; import { ISequence, ISplice } from 'vs/base/common/sequence'; import { ResourceTree, IBranchNode, isBranchNode, INode } from 'vs/base/common/resourceTree'; import { ObjectTree, ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { Iterator } from 'vs/base/common/iterator'; import * as paths from 'vs/base/common/path'; -import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { ICompressedTreeNode, ICompressedTreeElement, compress } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { URI } from 'vs/base/common/uri'; +import { FileKind } from 'vs/platform/files/common/files'; +import { compareFileNames } from 'vs/base/common/comparers'; export interface ISpliceEvent { index: number; @@ -436,35 +438,6 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer, void, FolderTemplate> { - - static TEMPLATE_ID = 'folder'; - get templateId(): string { return FolderRenderer.TEMPLATE_ID; } - - renderTemplate(container: HTMLElement): FolderTemplate { - const element = append(container, $('.resource-folder')); - const name = append(element, $('.name')); - - return { name }; - } - - renderElement(node: ITreeNode>, index: number, templateData: FolderTemplate): void { - templateData.name.textContent = node.element.name; - } - - renderCompressedElements(node: ITreeNode>, void>, index: number, templateData: FolderTemplate, height: number | undefined): void { - templateData.name.textContent = node.element.elements.map(e => e.name).join('/'); - } - - disposeTemplate(): void { - // noop - } -} - interface ResourceTemplate { element: HTMLElement; name: HTMLElement; @@ -497,7 +470,7 @@ class MultipleSelectionActionRunner extends ActionRunner { } } -class ResourceRenderer implements ICompressibleTreeRenderer { +class ResourceRenderer implements ICompressibleTreeRenderer, void, ResourceTemplate> { static TEMPLATE_ID = 'resource'; get templateId(): string { return ResourceRenderer.TEMPLATE_ID; } @@ -530,41 +503,66 @@ class ResourceRenderer implements ICompressibleTreeRenderer, index: number, template: ResourceTemplate): void { + renderElement(node: ITreeNode | ITreeNode>, index: number, template: ResourceTemplate): void { template.elementDisposable.dispose(); const resource = node.element; const theme = this.themeService.getTheme(); - const icon = theme.type === LIGHT ? resource.decorations.icon : resource.decorations.iconDark; + const icon = isBranchNode(resource) ? undefined : (theme.type === LIGHT ? resource.decorations.icon : resource.decorations.iconDark); - template.fileLabel.setFile(resource.sourceUri, { fileDecorations: { colors: false, badges: !icon }, hidePath: true }); + const uri = isBranchNode(resource) ? URI.file(resource.path) : resource.sourceUri; + const fileKind = isBranchNode(resource) ? FileKind.FOLDER : FileKind.FILE; + template.fileLabel.setFile(uri, { fileDecorations: { colors: false, badges: !icon }, hidePath: true, fileKind }); template.actionBar.clear(); template.actionBar.context = resource; const disposables = new DisposableStore(); - disposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceMenu(resource.resourceGroup), template.actionBar)); - toggleClass(template.name, 'strike-through', resource.decorations.strikeThrough); - toggleClass(template.element, 'faded', resource.decorations.faded); + if (!isBranchNode(resource)) { + disposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceMenu(resource.resourceGroup), template.actionBar)); + toggleClass(template.name, 'strike-through', resource.decorations.strikeThrough); + toggleClass(template.element, 'faded', resource.decorations.faded); + } + + const tooltip = (isBranchNode(resource) ? resource.path : resource.decorations.tooltip) || ''; if (icon) { template.decorationIcon.style.display = ''; template.decorationIcon.style.backgroundImage = `url('${icon}')`; - template.decorationIcon.title = resource.decorations.tooltip || ''; + template.decorationIcon.title = tooltip; } else { template.decorationIcon.style.display = 'none'; template.decorationIcon.style.backgroundImage = ''; } - template.element.setAttribute('data-tooltip', resource.decorations.tooltip || ''); + template.element.setAttribute('data-tooltip', tooltip); template.elementDisposable = disposables; } - renderCompressedElements(node: ITreeNode, void>, index: number, templateData: ResourceTemplate, height: number | undefined): void { - throw new Error('Should never happen since node is incompressible'); + renderCompressedElements(node: ITreeNode | ICompressedTreeNode>, void>, index: number, template: ResourceTemplate, height: number | undefined): void { + template.elementDisposable.dispose(); + + const compressed = node.element as ICompressedTreeNode>; + const resource = compressed.elements[compressed.elements.length - 1]; + + const label = compressed.elements.map(e => e.name).join('/'); + const uri = URI.file(resource.path); + const fileKind = FileKind.FOLDER; + template.fileLabel.setResource({ resource: uri, name: label }, { fileDecorations: { colors: false, badges: true }, fileKind }); + template.actionBar.clear(); + template.actionBar.context = resource; + + const disposables = new DisposableStore(); + + template.decorationIcon.style.display = 'none'; + template.decorationIcon.style.backgroundImage = ''; + + template.element.setAttribute('data-tooltip', resource.path); + template.elementDisposable = disposables; + } - disposeElement(resource: ITreeNode, index: number, template: ResourceTemplate): void { + disposeElement(resource: ITreeNode | ITreeNode>, index: number, template: ResourceTemplate): void { template.elementDisposable.dispose(); } @@ -579,9 +577,7 @@ class ProviderListDelegate implements IListVirtualDelegate { getHeight() { return 22; } getTemplateId(element: TreeElement) { - if (isBranchNode(element)) { - return FolderRenderer.TEMPLATE_ID; - } else if (isSCMResource(element)) { + if (isBranchNode(element) || isSCMResource(element)) { return ResourceRenderer.TEMPLATE_ID; } else { return ResourceGroupRenderer.TEMPLATE_ID; @@ -602,6 +598,27 @@ class SCMTreeFilter implements ITreeFilter { } } +export class SCMTreeSorter implements ITreeSorter { + + compare(one: TreeElement, other: TreeElement): number { + if (isSCMResourceGroup(one) && isSCMResourceGroup(other)) { + return 0; + } + + const oneIsDirectory = isBranchNode(one); + const otherIsDirectory = isBranchNode(other); + + if (oneIsDirectory !== otherIsDirectory) { + return oneIsDirectory ? -1 : 1; + } + + const oneName = isBranchNode(one) ? one.name : basename((one as ISCMResource).sourceUri); + const otherName = isBranchNode(other) ? other.name : basename((other as ISCMResource).sourceUri); + + return compareFileNames(oneName, otherName); + } +} + const scmResourceIdentityProvider = new class implements IIdentityProvider { getId(e: TreeElement): string { if (isBranchNode(e)) { @@ -935,11 +952,11 @@ export class RepositoryPanel extends ViewletPanel { const renderers = [ new ResourceGroupRenderer(actionViewItemProvider, this.themeService, this.menus), - new FolderRenderer(), new ResourceRenderer(this.listLabels, actionViewItemProvider, () => this.getSelectedResources(), this.themeService, this.menus) ]; const filter = new SCMTreeFilter(); + const sorter = new SCMTreeSorter(); this.tree = this.instantiationService.createInstance( WorkbenchCompressibleObjectTree, @@ -951,7 +968,8 @@ export class RepositoryPanel extends ViewletPanel { identityProvider: scmResourceIdentityProvider, keyboardNavigationLabelProvider: scmKeyboardNavigationLabelProvider, horizontalScrolling: false, - filter + filter, + sorter }); this._register(Event.chain(this.tree.onDidOpen) From aaa40e5c77d23948f98397667a10e5848d0ff1f4 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 17 Sep 2019 13:54:39 +0200 Subject: [PATCH 13/42] scm: keyboard nav --- .../contrib/scm/browser/scmViewlet.ts | 72 +++++++++++-------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index b8a9965b357..0981ade923a 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -54,11 +54,11 @@ import { ISequence, ISplice } from 'vs/base/common/sequence'; import { ResourceTree, IBranchNode, isBranchNode, INode } from 'vs/base/common/resourceTree'; import { ObjectTree, ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { Iterator } from 'vs/base/common/iterator'; -import * as paths from 'vs/base/common/path'; -import { ICompressedTreeNode, ICompressedTreeElement, compress } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { URI } from 'vs/base/common/uri'; import { FileKind } from 'vs/platform/files/common/files'; import { compareFileNames } from 'vs/base/common/comparers'; +import { FuzzyScore, createMatches } from 'vs/base/common/filters'; export interface ISpliceEvent { index: number; @@ -378,7 +378,7 @@ interface ResourceGroupTemplate { dispose: () => void; } -class ResourceGroupRenderer implements ICompressibleTreeRenderer { +class ResourceGroupRenderer implements ICompressibleTreeRenderer { static TEMPLATE_ID = 'resource group'; get templateId(): string { return ResourceGroupRenderer.TEMPLATE_ID; } @@ -407,7 +407,7 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer, index: number, template: ResourceGroupTemplate): void { + renderElement(node: ITreeNode, index: number, template: ResourceGroupTemplate): void { template.elementDisposable.dispose(); const group = node.element; @@ -425,11 +425,11 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer, void>, index: number, templateData: ResourceGroupTemplate, height: number | undefined): void { + renderCompressedElements(node: ITreeNode, FuzzyScore>, index: number, templateData: ResourceGroupTemplate, height: number | undefined): void { throw new Error('Should never happen since node is incompressible'); } - disposeElement(group: ITreeNode, index: number, template: ResourceGroupTemplate): void { + disposeElement(group: ITreeNode, index: number, template: ResourceGroupTemplate): void { template.elementDisposable.dispose(); } @@ -470,7 +470,7 @@ class MultipleSelectionActionRunner extends ActionRunner { } } -class ResourceRenderer implements ICompressibleTreeRenderer, void, ResourceTemplate> { +class ResourceRenderer implements ICompressibleTreeRenderer, FuzzyScore, ResourceTemplate> { static TEMPLATE_ID = 'resource'; get templateId(): string { return ResourceRenderer.TEMPLATE_ID; } @@ -486,7 +486,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer | ITreeNode>, index: number, template: ResourceTemplate): void { + renderElement(node: ITreeNode | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { template.elementDisposable.dispose(); const resource = node.element; @@ -512,7 +512,12 @@ class ResourceRenderer implements ICompressibleTreeRenderer | ICompressedTreeNode>, void>, index: number, template: ResourceTemplate, height: number | undefined): void { + renderCompressedElements(node: ITreeNode | ICompressedTreeNode>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { template.elementDisposable.dispose(); const compressed = node.element as ICompressedTreeNode>; @@ -548,7 +553,11 @@ class ResourceRenderer implements ICompressibleTreeRenderer e.name).join('/'); const uri = URI.file(resource.path); const fileKind = FileKind.FOLDER; - template.fileLabel.setResource({ resource: uri, name: label }, { fileDecorations: { colors: false, badges: true }, fileKind }); + template.fileLabel.setResource({ resource: uri, name: label }, { + fileDecorations: { colors: false, badges: true }, + fileKind, + matches: createMatches(node.filterData) + }); template.actionBar.clear(); template.actionBar.context = resource; @@ -562,7 +571,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer | ITreeNode>, index: number, template: ResourceTemplate): void { + disposeElement(resource: ITreeNode | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { template.elementDisposable.dispose(); } @@ -619,6 +628,21 @@ export class SCMTreeSorter implements ITreeSorter { } } +export class SCMTreeKeyboardNavigationLabelProvider implements IKeyboardNavigationLabelProvider { + + getKeyboardNavigationLabel(element: TreeElement): { toString(): string; } | undefined { + if (isSCMResourceGroup(element)) { + return element.label; + } + + if (isSCMResource(element)) { + return basename(element.sourceUri); + } + + return ''; + } +} + const scmResourceIdentityProvider = new class implements IIdentityProvider { getId(e: TreeElement): string { if (isBranchNode(e)) { @@ -634,18 +658,6 @@ const scmResourceIdentityProvider = new class implements IIdentityProvider { - getKeyboardNavigationLabel(e: TreeElement) { - if (isBranchNode(e)) { - return paths.posix.basename(e.path); - } else if (isSCMResource(e)) { - return basename(e.sourceUri); - } else { - return e.label; - } - } -}; - // function isGroupVisible(group: ISCMResourceGroup) { // return group.elements.length > 0 || !group.hideWhenEmpty; // } @@ -678,12 +690,13 @@ class ResourceGroupSplicer { constructor( groupSequence: ISequence, - private tree: ObjectTree + private tree: ObjectTree ) { groupSequence.onDidSplice(this.onDidSpliceGroups, this, this.disposables); this.onDidSpliceGroups({ start: 0, deleteCount: 0, toInsert: groupSequence.elements }); } + // TODO@joao: optimize private fullRefresh(): void { this.tree.setChildren(null, this.items.map(item => { return { @@ -820,7 +833,7 @@ export class RepositoryPanel extends ViewletPanel { private inputBoxContainer: HTMLElement; private inputBox: InputBox; private listContainer: HTMLElement; - private tree: ObjectTree; + private tree: ObjectTree; private listLabels: ResourceLabels; private menus: SCMMenus; private visibilityDisposables: IDisposable[] = []; @@ -957,6 +970,7 @@ export class RepositoryPanel extends ViewletPanel { const filter = new SCMTreeFilter(); const sorter = new SCMTreeSorter(); + const keyboardNavigationLabelProvider = new SCMTreeKeyboardNavigationLabelProvider(); this.tree = this.instantiationService.createInstance( WorkbenchCompressibleObjectTree, @@ -966,10 +980,10 @@ export class RepositoryPanel extends ViewletPanel { renderers, { identityProvider: scmResourceIdentityProvider, - keyboardNavigationLabelProvider: scmKeyboardNavigationLabelProvider, horizontalScrolling: false, filter, - sorter + sorter, + keyboardNavigationLabelProvider }); this._register(Event.chain(this.tree.onDidOpen) From ab1fe2a0c270146d28cc88f897b150a9c1064f28 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 17 Sep 2019 15:54:48 +0200 Subject: [PATCH 14/42] scm: flat --- .../workbench/contrib/scm/browser/scmViewlet.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index 0981ade923a..8ac0a9e0710 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -685,6 +685,7 @@ function asTreeElement(node: INode, incompressible: boolean): ICom class ResourceGroupSplicer { + private flat = true; private items: IGroupItem[] = []; private disposables = new DisposableStore(); @@ -698,13 +699,19 @@ class ResourceGroupSplicer { // TODO@joao: optimize private fullRefresh(): void { - this.tree.setChildren(null, this.items.map(item => { - return { + if (this.flat) { + this.tree.setChildren(null, this.items.map(item => ({ + element: item.group, + children: Iterator.map(Iterator.fromArray(item.resources), element => ({ element, incompressible: true })), + incompressible: true + }))); + } else { + this.tree.setChildren(null, this.items.map(item => ({ element: item.group, children: Iterator.map(item.tree.root.children, node => asTreeElement(node, true)), incompressible: true - } as ICompressedTreeElement; - })); + }))); + } } private onDidSpliceGroups({ start, deleteCount, toInsert }: ISplice): void { From 0b795b1bec83c6a07962d74acb68be1c61b48334 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 17 Sep 2019 18:45:48 +0200 Subject: [PATCH 15/42] scm: reorg --- .../browser/{scmActivity.ts => activity.ts} | 0 .../contrib/scm/browser/mainPanel.ts | 329 +++++ .../scm/browser/{scmMenus.ts => menus.ts} | 2 +- .../contrib/scm/browser/repositoryPanel.ts | 843 ++++++++++++ .../workbench/contrib/scm/browser/scmUtil.ts | 18 - .../contrib/scm/browser/scmViewlet.ts | 1156 +---------------- src/vs/workbench/contrib/scm/browser/util.ts | 53 + 7 files changed, 1238 insertions(+), 1163 deletions(-) rename src/vs/workbench/contrib/scm/browser/{scmActivity.ts => activity.ts} (100%) create mode 100644 src/vs/workbench/contrib/scm/browser/mainPanel.ts rename src/vs/workbench/contrib/scm/browser/{scmMenus.ts => menus.ts} (99%) create mode 100644 src/vs/workbench/contrib/scm/browser/repositoryPanel.ts delete mode 100644 src/vs/workbench/contrib/scm/browser/scmUtil.ts create mode 100644 src/vs/workbench/contrib/scm/browser/util.ts diff --git a/src/vs/workbench/contrib/scm/browser/scmActivity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts similarity index 100% rename from src/vs/workbench/contrib/scm/browser/scmActivity.ts rename to src/vs/workbench/contrib/scm/browser/activity.ts diff --git a/src/vs/workbench/contrib/scm/browser/mainPanel.ts b/src/vs/workbench/contrib/scm/browser/mainPanel.ts new file mode 100644 index 00000000000..8bad75a2aa1 --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/mainPanel.ts @@ -0,0 +1,329 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/scmViewlet'; +import { localize } from 'vs/nls'; +import { Event, Emitter } from 'vs/base/common/event'; +import { basename } from 'vs/base/common/resources'; +import { IDisposable, dispose, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; +import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; +import { append, $, toggleClass } from 'vs/base/browser/dom'; +import { List } from 'vs/base/browser/ui/list/listWidget'; +import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list'; +import { ISCMService, ISCMRepository } from 'vs/workbench/contrib/scm/common/scm'; +import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IAction, Action } from 'vs/base/common/actions'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; +import { Command } from 'vs/editor/common/modes'; +import { renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IViewDescriptor } from 'vs/workbench/common/views'; + +export interface ISpliceEvent { + index: number; + deleteCount: number; + elements: T[]; +} + +export interface IViewModel { + readonly repositories: ISCMRepository[]; + readonly onDidSplice: Event>; + + readonly visibleRepositories: ISCMRepository[]; + readonly onDidChangeVisibleRepositories: Event; + setVisibleRepositories(repositories: ISCMRepository[]): void; + + isVisible(): boolean; + readonly onDidChangeVisibility: Event; +} + +class ProvidersListDelegate implements IListVirtualDelegate { + + getHeight(): number { + return 22; + } + + getTemplateId(): string { + return 'provider'; + } +} + +class StatusBarAction extends Action { + + constructor( + private command: Command, + private commandService: ICommandService + ) { + super(`statusbaraction{${command.id}}`, command.title, '', true); + this.tooltip = command.tooltip || ''; + } + + run(): Promise { + return this.commandService.executeCommand(this.command.id, ...(this.command.arguments || [])); + } +} + +class StatusBarActionViewItem extends ActionViewItem { + + constructor(action: StatusBarAction) { + super(null, action, {}); + } + + updateLabel(): void { + if (this.options.label) { + this.label.innerHTML = renderOcticons(this.getAction().label); + } + } +} + +interface RepositoryTemplateData { + title: HTMLElement; + type: HTMLElement; + countContainer: HTMLElement; + count: CountBadge; + actionBar: ActionBar; + disposable: IDisposable; + templateDisposable: IDisposable; +} + +class ProviderRenderer implements IListRenderer { + + readonly templateId = 'provider'; + + private _onDidRenderElement = new Emitter(); + readonly onDidRenderElement = this._onDidRenderElement.event; + + constructor( + @ICommandService protected commandService: ICommandService, + @IThemeService protected themeService: IThemeService + ) { } + + renderTemplate(container: HTMLElement): RepositoryTemplateData { + const provider = append(container, $('.scm-provider')); + const name = append(provider, $('.name')); + const title = append(name, $('span.title')); + const type = append(name, $('span.type')); + const countContainer = append(provider, $('.count')); + const count = new CountBadge(countContainer); + const badgeStyler = attachBadgeStyler(count, this.themeService); + const actionBar = new ActionBar(provider, { actionViewItemProvider: a => new StatusBarActionViewItem(a as StatusBarAction) }); + const disposable = Disposable.None; + const templateDisposable = combinedDisposable(actionBar, badgeStyler); + + return { title, type, countContainer, count, actionBar, disposable, templateDisposable }; + } + + renderElement(repository: ISCMRepository, index: number, templateData: RepositoryTemplateData): void { + templateData.disposable.dispose(); + const disposables = new DisposableStore(); + + if (repository.provider.rootUri) { + templateData.title.textContent = basename(repository.provider.rootUri); + templateData.type.textContent = repository.provider.label; + } else { + templateData.title.textContent = repository.provider.label; + templateData.type.textContent = ''; + } + + const actions: IAction[] = []; + const disposeActions = () => dispose(actions); + disposables.add({ dispose: disposeActions }); + + const update = () => { + disposeActions(); + + const commands = repository.provider.statusBarCommands || []; + actions.splice(0, actions.length, ...commands.map(c => new StatusBarAction(c, this.commandService))); + templateData.actionBar.clear(); + templateData.actionBar.push(actions); + + const count = repository.provider.count || 0; + toggleClass(templateData.countContainer, 'hidden', count === 0); + templateData.count.setCount(count); + + this._onDidRenderElement.fire(repository); + }; + + disposables.add(repository.provider.onDidChange(update, null)); + update(); + + templateData.disposable = disposables; + } + + disposeTemplate(templateData: RepositoryTemplateData): void { + templateData.disposable.dispose(); + templateData.templateDisposable.dispose(); + } +} + +export class MainPanel extends ViewletPanel { + + static readonly ID = 'scm.mainPanel'; + static readonly TITLE = localize('scm providers', "Source Control Providers"); + + private list: List; + + constructor( + protected viewModel: IViewModel, + options: IViewletPanelOptions, + @IKeybindingService protected keybindingService: IKeybindingService, + @IContextMenuService protected contextMenuService: IContextMenuService, + @ISCMService protected scmService: ISCMService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, + @IConfigurationService configurationService: IConfigurationService + ) { + super(options, keybindingService, contextMenuService, configurationService, contextKeyService); + } + + protected renderBody(container: HTMLElement): void { + const delegate = new ProvidersListDelegate(); + const renderer = this.instantiationService.createInstance(ProviderRenderer); + const identityProvider = { getId: (r: ISCMRepository) => r.provider.id }; + + this.list = this.instantiationService.createInstance(WorkbenchList, `SCM Main`, container, delegate, [renderer], { + identityProvider, + horizontalScrolling: false + }); + + this._register(renderer.onDidRenderElement(e => this.list.updateWidth(this.viewModel.repositories.indexOf(e)), null)); + this._register(this.list.onSelectionChange(this.onListSelectionChange, this)); + this._register(this.list.onFocusChange(this.onListFocusChange, this)); + this._register(this.list.onContextMenu(this.onListContextMenu, this)); + + this._register(this.viewModel.onDidChangeVisibleRepositories(this.updateListSelection, this)); + + this._register(this.viewModel.onDidSplice(({ index, deleteCount, elements }) => this.splice(index, deleteCount, elements), null)); + this.splice(0, 0, this.viewModel.repositories); + + this._register(this.list); + + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('scm.providers.visible')) { + this.updateBodySize(); + } + })); + + this.updateListSelection(); + } + + private splice(index: number, deleteCount: number, repositories: ISCMRepository[] = []): void { + this.list.splice(index, deleteCount, repositories); + + const empty = this.list.length === 0; + toggleClass(this.element, 'empty', empty); + + this.updateBodySize(); + } + + protected layoutBody(height: number, width: number): void { + this.list.layout(height, width); + } + + private updateBodySize(): void { + const visibleCount = this.configurationService.getValue('scm.providers.visible'); + const empty = this.list.length === 0; + const size = Math.min(this.viewModel.repositories.length, visibleCount) * 22; + + this.minimumBodySize = visibleCount === 0 ? 22 : size; + this.maximumBodySize = visibleCount === 0 ? Number.POSITIVE_INFINITY : empty ? Number.POSITIVE_INFINITY : size; + } + + private onListContextMenu(e: IListContextMenuEvent): void { + if (!e.element) { + return; + } + + const repository = e.element; + const contextKeyService = this.contextKeyService.createScoped(); + const scmProviderKey = contextKeyService.createKey('scmProvider', undefined); + scmProviderKey.set(repository.provider.contextValue); + + const menu = this.menuService.createMenu(MenuId.SCMSourceControl, contextKeyService); + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + const disposable = createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => g === 'inline'); + + menu.dispose(); + contextKeyService.dispose(); + + if (secondary.length === 0) { + return; + } + + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => secondary, + getActionsContext: () => repository.provider + }); + + disposable.dispose(); + } + + private onListSelectionChange(e: IListEvent): void { + if (e.browserEvent && e.elements.length > 0) { + const scrollTop = this.list.scrollTop; + this.viewModel.setVisibleRepositories(e.elements); + this.list.scrollTop = scrollTop; + } + } + + private onListFocusChange(e: IListEvent): void { + if (e.browserEvent && e.elements.length > 0) { + e.elements[0].focus(); + } + } + + private updateListSelection(): void { + const set = new Set(); + + for (const repository of this.viewModel.visibleRepositories) { + set.add(repository); + } + + const selection: number[] = []; + + for (let i = 0; i < this.list.length; i++) { + if (set.has(this.list.element(i))) { + selection.push(i); + } + } + + this.list.setSelection(selection); + + if (selection.length > 0) { + this.list.setFocus([selection[0]]); + } + } +} + +export class MainPanelDescriptor implements IViewDescriptor { + + readonly id = MainPanel.ID; + readonly name = MainPanel.TITLE; + readonly ctorDescriptor: { ctor: any, arguments?: any[] }; + readonly canToggleVisibility = true; + readonly hideByDefault = false; + readonly order = -1000; + readonly workspace = true; + readonly when = ContextKeyExpr.or(ContextKeyExpr.equals('config.scm.alwaysShowProviders', true), ContextKeyExpr.and(ContextKeyExpr.notEquals('scm.providerCount', 0), ContextKeyExpr.notEquals('scm.providerCount', 1))); + + constructor(viewModel: IViewModel) { + this.ctorDescriptor = { ctor: MainPanel, arguments: [viewModel] }; + } +} diff --git a/src/vs/workbench/contrib/scm/browser/scmMenus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts similarity index 99% rename from src/vs/workbench/contrib/scm/browser/scmMenus.ts rename to src/vs/workbench/contrib/scm/browser/menus.ts index 827765114ef..02a31f096d2 100644 --- a/src/vs/workbench/contrib/scm/browser/scmMenus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -11,7 +11,7 @@ import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions' import { IAction } from 'vs/base/common/actions'; import { createAndFillInContextMenuActions, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { ISCMProvider, ISCMResource, ISCMResourceGroup } from 'vs/workbench/contrib/scm/common/scm'; -import { isSCMResource } from './scmUtil'; +import { isSCMResource } from './util'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { equals } from 'vs/base/common/arrays'; import { ISplice } from 'vs/base/common/sequence'; diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts new file mode 100644 index 00000000000..c4cbe74efc1 --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -0,0 +1,843 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/scmViewlet'; +import { Event } from 'vs/base/common/event'; +import { domEvent } from 'vs/base/browser/event'; +import { basename, relativePath } from 'vs/base/common/resources'; +import { IDisposable, dispose, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; +import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; +import { append, $, addClass, toggleClass, trackFocus, removeClass } from 'vs/base/browser/dom'; +import { IListVirtualDelegate, IKeyboardNavigationLabelProvider, IIdentityProvider } from 'vs/base/browser/ui/list/list'; +import { ISCMRepository, ISCMResourceGroup, ISCMResource, InputValidationType } 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'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { MenuItemAction, IMenuService } from 'vs/platform/actions/common/actions'; +import { IAction, IActionViewItem, ActionRunner } from 'vs/base/common/actions'; +import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { SCMMenus } from './menus'; +import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IThemeService, LIGHT } from 'vs/platform/theme/common/themeService'; +import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar } from './util'; +import { attachBadgeStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler'; +import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; +import { format } from 'vs/base/common/strings'; +import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ThrottledDelayer } 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 } from 'vs/base/browser/ui/tree/tree'; +import { ISequence, ISplice } from 'vs/base/common/sequence'; +import { ResourceTree, IBranchNode, isBranchNode, INode } from 'vs/base/common/resourceTree'; +import { ObjectTree, ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; +import { Iterator } from 'vs/base/common/iterator'; +import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { URI } from 'vs/base/common/uri'; +import { FileKind } from 'vs/platform/files/common/files'; +import { compareFileNames } from 'vs/base/common/comparers'; +import { FuzzyScore, createMatches } from 'vs/base/common/filters'; +import { IViewDescriptor } from 'vs/workbench/common/views'; + +interface ResourceGroupTemplate { + name: HTMLElement; + count: CountBadge; + actionBar: ActionBar; + elementDisposable: IDisposable; + dispose: () => void; +} + +class ResourceGroupRenderer implements ICompressibleTreeRenderer { + + static TEMPLATE_ID = 'resource group'; + get templateId(): string { return ResourceGroupRenderer.TEMPLATE_ID; } + + constructor( + private actionViewItemProvider: IActionViewItemProvider, + private themeService: IThemeService, + private menus: SCMMenus + ) { } + + renderTemplate(container: HTMLElement): ResourceGroupTemplate { + const element = append(container, $('.resource-group')); + const name = append(element, $('.name')); + const actionsContainer = append(element, $('.actions')); + const actionBar = new ActionBar(actionsContainer, { actionViewItemProvider: this.actionViewItemProvider }); + const countContainer = append(element, $('.count')); + const count = new CountBadge(countContainer); + const styler = attachBadgeStyler(count, this.themeService); + const elementDisposable = Disposable.None; + + return { + name, count, actionBar, elementDisposable, dispose: () => { + actionBar.dispose(); + styler.dispose(); + } + }; + } + + renderElement(node: ITreeNode, index: number, template: ResourceGroupTemplate): void { + template.elementDisposable.dispose(); + + const group = node.element; + template.name.textContent = group.label; + template.actionBar.clear(); + template.actionBar.context = group; + + const disposables = new DisposableStore(); + disposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceGroupMenu(group), template.actionBar)); + + const updateCount = () => template.count.setCount(group.elements.length); + disposables.add(group.onDidSplice(updateCount, null)); + updateCount(); + + template.elementDisposable = disposables; + } + + renderCompressedElements(node: ITreeNode, FuzzyScore>, index: number, templateData: ResourceGroupTemplate, height: number | undefined): void { + throw new Error('Should never happen since node is incompressible'); + } + + disposeElement(group: ITreeNode, index: number, template: ResourceGroupTemplate): void { + template.elementDisposable.dispose(); + } + + disposeTemplate(template: ResourceGroupTemplate): void { + template.dispose(); + } +} + +interface ResourceTemplate { + element: HTMLElement; + name: HTMLElement; + fileLabel: IResourceLabel; + decorationIcon: HTMLElement; + actionBar: ActionBar; + elementDisposable: IDisposable; + dispose: () => void; +} + +class MultipleSelectionActionRunner extends ActionRunner { + + constructor(private getSelectedResources: () => ISCMResource[]) { + super(); + } + + runAction(action: IAction, context: ISCMResource): Promise { + if (action instanceof MenuItemAction) { + const selection = this.getSelectedResources(); + const filteredSelection = selection.filter(s => s !== context); + + if (selection.length === filteredSelection.length || selection.length === 1) { + return action.run(context); + } + + return action.run(context, ...filteredSelection); + } + + return super.runAction(action, context); + } +} + +class ResourceRenderer implements ICompressibleTreeRenderer, FuzzyScore, ResourceTemplate> { + + static TEMPLATE_ID = 'resource'; + get templateId(): string { return ResourceRenderer.TEMPLATE_ID; } + + constructor( + private labels: ResourceLabels, + private actionViewItemProvider: IActionViewItemProvider, + private getSelectedResources: () => ISCMResource[], + private themeService: IThemeService, + private menus: SCMMenus + ) { } + + renderTemplate(container: HTMLElement): ResourceTemplate { + const element = append(container, $('.resource')); + const name = append(element, $('.name')); + const fileLabel = this.labels.create(name, { supportHighlights: true }); + const actionsContainer = append(fileLabel.element, $('.actions')); + const actionBar = new ActionBar(actionsContainer, { + actionViewItemProvider: this.actionViewItemProvider, + actionRunner: new MultipleSelectionActionRunner(this.getSelectedResources) + }); + + const decorationIcon = append(element, $('.decoration-icon')); + + return { + element, name, fileLabel, decorationIcon, actionBar, elementDisposable: Disposable.None, dispose: () => { + actionBar.dispose(); + fileLabel.dispose(); + } + }; + } + + renderElement(node: ITreeNode | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { + template.elementDisposable.dispose(); + + const resource = node.element; + const theme = this.themeService.getTheme(); + const icon = isBranchNode(resource) ? undefined : (theme.type === LIGHT ? resource.decorations.icon : resource.decorations.iconDark); + + const uri = isBranchNode(resource) ? URI.file(resource.path) : resource.sourceUri; + const fileKind = isBranchNode(resource) ? FileKind.FOLDER : FileKind.FILE; + template.fileLabel.setFile(uri, { + fileDecorations: { colors: false, badges: !icon }, + hidePath: true, + fileKind, + matches: createMatches(node.filterData) + }); + template.actionBar.clear(); + template.actionBar.context = resource; + + const disposables = new DisposableStore(); + + if (!isBranchNode(resource)) { + disposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceMenu(resource.resourceGroup), template.actionBar)); + toggleClass(template.name, 'strike-through', resource.decorations.strikeThrough); + toggleClass(template.element, 'faded', resource.decorations.faded); + } + + const tooltip = (isBranchNode(resource) ? resource.path : resource.decorations.tooltip) || ''; + + if (icon) { + template.decorationIcon.style.display = ''; + template.decorationIcon.style.backgroundImage = `url('${icon}')`; + template.decorationIcon.title = tooltip; + } else { + template.decorationIcon.style.display = 'none'; + template.decorationIcon.style.backgroundImage = ''; + } + + template.element.setAttribute('data-tooltip', tooltip); + template.elementDisposable = disposables; + } + + renderCompressedElements(node: ITreeNode | ICompressedTreeNode>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { + template.elementDisposable.dispose(); + + const compressed = node.element as ICompressedTreeNode>; + const resource = compressed.elements[compressed.elements.length - 1]; + + const label = compressed.elements.map(e => e.name).join('/'); + const uri = URI.file(resource.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.decorationIcon.style.display = 'none'; + template.decorationIcon.style.backgroundImage = ''; + + template.element.setAttribute('data-tooltip', resource.path); + template.elementDisposable = disposables; + + } + + disposeElement(resource: ITreeNode | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { + template.elementDisposable.dispose(); + } + + disposeTemplate(template: ResourceTemplate): void { + template.elementDisposable.dispose(); + template.dispose(); + } +} + +class ProviderListDelegate implements IListVirtualDelegate { + + getHeight() { return 22; } + + getTemplateId(element: TreeElement) { + if (isBranchNode(element) || isSCMResource(element)) { + return ResourceRenderer.TEMPLATE_ID; + } else { + return ResourceGroupRenderer.TEMPLATE_ID; + } + } +} + +class SCMTreeFilter implements ITreeFilter { + + filter(element: TreeElement): boolean { + if (isBranchNode(element)) { + return true; + } else if (isSCMResourceGroup(element)) { + return element.elements.length > 0 || !element.hideWhenEmpty; + } else { + return true; + } + } +} + +export class SCMTreeSorter implements ITreeSorter { + + compare(one: TreeElement, other: TreeElement): number { + if (isSCMResourceGroup(one) && isSCMResourceGroup(other)) { + return 0; + } + + const oneIsDirectory = isBranchNode(one); + const otherIsDirectory = isBranchNode(other); + + if (oneIsDirectory !== otherIsDirectory) { + return oneIsDirectory ? -1 : 1; + } + + const oneName = isBranchNode(one) ? one.name : basename((one as ISCMResource).sourceUri); + const otherName = isBranchNode(other) ? other.name : basename((other as ISCMResource).sourceUri); + + return compareFileNames(oneName, otherName); + } +} + +export class SCMTreeKeyboardNavigationLabelProvider implements IKeyboardNavigationLabelProvider { + + getKeyboardNavigationLabel(element: TreeElement): { toString(): string; } | undefined { + if (isSCMResourceGroup(element)) { + return element.label; + } + + if (isSCMResource(element)) { + return basename(element.sourceUri); + } + + return ''; + } +} + +const scmResourceIdentityProvider = new class implements IIdentityProvider { + getId(e: TreeElement): string { + if (isBranchNode(e)) { + return e.path; + } else if (isSCMResource(e)) { + const group = e.resourceGroup; + const provider = group.provider; + return `${provider.contextValue}/${group.id}/${e.sourceUri.toString()}`; + } else { + const provider = e.provider; + return `${provider.contextValue}/${e.id}`; + } + } +}; + +// function isGroupVisible(group: ISCMResourceGroup) { +// return group.elements.length > 0 || !group.hideWhenEmpty; +// } + +interface IGroupItem { + readonly group: ISCMResourceGroup; + readonly resources: ISCMResource[]; + readonly tree: ResourceTree; + // visible: boolean; + readonly disposable: IDisposable; +} + +function asTreeElement(node: INode, incompressible: boolean): ICompressedTreeElement { + if (isBranchNode(node)) { + return { + element: node, + children: Iterator.map(node.children, node => asTreeElement(node, false)), + incompressible, + collapsed: false + }; + } + + return { element: node.element, incompressible: true }; +} + +class ResourceGroupSplicer { + + private flat = false; + private items: IGroupItem[] = []; + private disposables = new DisposableStore(); + + constructor( + groupSequence: ISequence, + private tree: ObjectTree + ) { + groupSequence.onDidSplice(this.onDidSpliceGroups, this, this.disposables); + this.onDidSpliceGroups({ start: 0, deleteCount: 0, toInsert: groupSequence.elements }); + } + + // TODO@joao: optimize + private fullRefresh(): void { + if (this.flat) { + this.tree.setChildren(null, this.items.map(item => ({ + element: item.group, + children: Iterator.map(Iterator.fromArray(item.resources), element => ({ element, incompressible: true })), + incompressible: true + }))); + } else { + this.tree.setChildren(null, this.items.map(item => ({ + element: item.group, + children: Iterator.map(item.tree.root.children, node => asTreeElement(node, true)), + incompressible: true + }))); + } + } + + private onDidSpliceGroups({ start, deleteCount, toInsert }: ISplice): void { + const itemsToInsert: IGroupItem[] = []; + + for (const group of toInsert) { + const tree = new ResourceTree(); + const resources: ISCMResource[] = [...group.elements]; + const disposable = combinedDisposable( + group.onDidChange(() => this.onDidChangeGroup()), + group.onDidSplice(splice => this.onDidSpliceGroup(item, splice)) + ); + const item = { group, resources, tree, disposable }; + + itemsToInsert.push(item); + } + + const itemsToDispose = this.items.splice(start, deleteCount, ...itemsToInsert); + + for (const item of itemsToDispose) { + item.disposable.dispose(); + } + + this.fullRefresh(); + } + + private onDidChangeGroup(): void { + this.fullRefresh(); + // const itemIndex = firstIndex(this.items, item => item.group === group); + + // if (itemIndex < 0) { + // return; + // } + + // const item = this.items[itemIndex]; + // const visible = isGroupVisible(group); + + // if (item.visible === visible) { + // return; + // } + + // let absoluteStart = 0; + + // for (let i = 0; i < itemIndex; i++) { + // const item = this.items[i]; + // absoluteStart += (item.visible ? 1 : 0) + item.group.elements.length; + // } + + // if (visible) { + // this.spliceable.splice(absoluteStart, 0, [group, ...group.elements]); + // } else { + // this.spliceable.splice(absoluteStart, 1 + group.elements.length, []); + // } + + // item.visible = visible; + } + + private onDidSpliceGroup(item: IGroupItem, { start, deleteCount, toInsert }: ISplice): void { + const root = item.group.provider.rootUri || URI.file('/'); + + for (const resource of toInsert) { + item.tree.add(relativePath(root, resource.sourceUri) || resource.sourceUri.fsPath, resource); + } + + const deleted = item.resources.splice(start, deleteCount, ...toInsert); + + for (const resource of deleted) { + item.tree.delete(relativePath(root, resource.sourceUri) || resource.sourceUri.fsPath); + } + + this.fullRefresh(); + // const itemIndex = firstIndex(this.items, item => item.group === group); + + // if (itemIndex < 0) { + // return; + // } + + // const item = this.items[itemIndex]; + // const visible = isGroupVisible(group); + + // if (!item.visible && !visible) { + // return; + // } + + // let absoluteStart = start; + + // for (let i = 0; i < itemIndex; i++) { + // const item = this.items[i]; + // absoluteStart += (item.visible ? 1 : 0) + item.group.elements.length; + // } + + // if (item.visible && !visible) { + // this.spliceable.splice(absoluteStart, 1 + deleteCount, toInsert); + // } else if (!item.visible && visible) { + // this.spliceable.splice(absoluteStart, deleteCount, [group, ...toInsert]); + // } else { + // this.spliceable.splice(absoluteStart + 1, deleteCount, toInsert); + // } + + // item.visible = visible; + + + } + + dispose(): void { + this.onDidSpliceGroups({ start: 0, deleteCount: this.items.length, toInsert: [] }); + this.disposables = dispose(this.disposables); + } +} + +function convertValidationType(type: InputValidationType): MessageType { + switch (type) { + case InputValidationType.Information: return MessageType.INFO; + case InputValidationType.Warning: return MessageType.WARNING; + case InputValidationType.Error: return MessageType.ERROR; + } +} + +type TreeElement = ISCMResourceGroup | IBranchNode | ISCMResource; + +export class RepositoryPanel extends ViewletPanel { + + private cachedHeight: number | undefined = undefined; + private cachedWidth: number | undefined = undefined; + private cachedScrollTop: number | undefined = undefined; + private inputBoxContainer: HTMLElement; + private inputBox: InputBox; + private listContainer: HTMLElement; + private tree: ObjectTree; + private listLabels: ResourceLabels; + private menus: SCMMenus; + private visibilityDisposables: IDisposable[] = []; + protected contextKeyService: IContextKeyService; + + constructor( + readonly repository: ISCMRepository, + options: IViewletPanelOptions, + @IKeybindingService protected keybindingService: IKeybindingService, + @IThemeService protected themeService: IThemeService, + @IContextMenuService protected contextMenuService: IContextMenuService, + @IContextViewService protected contextViewService: IContextViewService, + @ICommandService protected commandService: ICommandService, + @INotificationService private readonly notificationService: INotificationService, + @IEditorService protected editorService: IEditorService, + @IInstantiationService protected instantiationService: IInstantiationService, + @IConfigurationService protected configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IMenuService protected menuService: IMenuService + ) { + super(options, keybindingService, contextMenuService, configurationService, contextKeyService); + + this.menus = instantiationService.createInstance(SCMMenus, this.repository.provider); + this._register(this.menus); + this._register(this.menus.onDidChangeTitle(this._onDidChangeTitleArea.fire, this._onDidChangeTitleArea)); + + this.contextKeyService = contextKeyService.createScoped(this.element); + this.contextKeyService.createKey('scmRepository', this.repository); + } + + render(): void { + super.render(); + this._register(this.menus.onDidChangeTitle(this.updateActions, this)); + } + + protected renderHeaderTitle(container: HTMLElement): void { + let title: string; + let type: string; + + if (this.repository.provider.rootUri) { + title = basename(this.repository.provider.rootUri); + type = this.repository.provider.label; + } else { + title = this.repository.provider.label; + type = ''; + } + + super.renderHeaderTitle(container, title); + addClass(container, 'scm-provider'); + append(container, $('span.type', undefined, type)); + } + + protected renderBody(container: HTMLElement): void { + const focusTracker = trackFocus(container); + this._register(focusTracker.onDidFocus(() => this.repository.focus())); + this._register(focusTracker); + + // Input + this.inputBoxContainer = append(container, $('.scm-editor')); + + const updatePlaceholder = () => { + const binding = this.keybindingService.lookupKeybinding('scm.acceptInput'); + const label = binding ? binding.getLabel() : (platform.isMacintosh ? 'Cmd+Enter' : 'Ctrl+Enter'); + const placeholder = format(this.repository.input.placeholder, label); + + this.inputBox.setPlaceHolder(placeholder); + }; + + const validationDelayer = new ThrottledDelayer(200); + const validate = () => { + return this.repository.input.validateInput(this.inputBox.value, this.inputBox.inputElement.selectionStart || 0).then(result => { + if (!result) { + this.inputBox.inputElement.removeAttribute('aria-invalid'); + this.inputBox.hideMessage(); + } else { + this.inputBox.inputElement.setAttribute('aria-invalid', 'true'); + this.inputBox.showMessage({ content: result.message, type: convertValidationType(result.type) }); + } + }); + }; + + const triggerValidation = () => validationDelayer.trigger(validate); + + this.inputBox = new InputBox(this.inputBoxContainer, this.contextViewService, { flexibleHeight: true, flexibleMaxHeight: 134 }); + this.inputBox.setEnabled(this.isBodyVisible()); + this._register(attachInputBoxStyler(this.inputBox, this.themeService)); + this._register(this.inputBox); + + this._register(this.inputBox.onDidChange(triggerValidation, null)); + + const onKeyUp = domEvent(this.inputBox.inputElement, 'keyup'); + const onMouseUp = domEvent(this.inputBox.inputElement, 'mouseup'); + this._register(Event.any(onKeyUp, onMouseUp)(triggerValidation, null)); + + this.inputBox.value = this.repository.input.value; + this._register(this.inputBox.onDidChange(value => this.repository.input.value = value, null)); + this._register(this.repository.input.onDidChange(value => this.inputBox.value = value, null)); + + updatePlaceholder(); + this._register(this.repository.input.onDidChangePlaceholder(updatePlaceholder, null)); + this._register(this.keybindingService.onDidUpdateKeybindings(updatePlaceholder, null)); + + this._register(this.inputBox.onDidHeightChange(() => this.layoutBody())); + + if (this.repository.provider.onDidChangeCommitTemplate) { + this._register(this.repository.provider.onDidChangeCommitTemplate(this.updateInputBox, this)); + } + + this.updateInputBox(); + + // Input box visibility + this._register(this.repository.input.onDidChangeVisibility(this.updateInputBoxVisibility, this)); + this.updateInputBoxVisibility(); + + // List + this.listContainer = append(container, $('.scm-status.show-file-icons')); + + const updateActionsVisibility = () => toggleClass(this.listContainer, 'show-actions', this.configurationService.getValue('scm.alwaysShowActions')); + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowActions'))(updateActionsVisibility); + updateActionsVisibility(); + + const delegate = new ProviderListDelegate(); + + const actionViewItemProvider = (action: IAction) => this.getActionViewItem(action); + + this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); + this._register(this.listLabels); + + const renderers = [ + new ResourceGroupRenderer(actionViewItemProvider, this.themeService, this.menus), + new ResourceRenderer(this.listLabels, actionViewItemProvider, () => this.getSelectedResources(), this.themeService, this.menus) + ]; + + const filter = new SCMTreeFilter(); + const sorter = new SCMTreeSorter(); + const keyboardNavigationLabelProvider = new SCMTreeKeyboardNavigationLabelProvider(); + + this.tree = this.instantiationService.createInstance( + WorkbenchCompressibleObjectTree, + `SCM Tree Repo`, + this.listContainer, + delegate, + renderers, + { + identityProvider: scmResourceIdentityProvider, + horizontalScrolling: false, + filter, + sorter, + keyboardNavigationLabelProvider + }); + + this._register(Event.chain(this.tree.onDidOpen) + .map(e => e.elements[0]) + .filter(e => !!e && !isBranchNode(e) && isSCMResource(e)) + .on(this.open, this)); + + // this._register(Event.chain(this.tree.onPin) + // .map(e => e.elements[0]) + // .filter(e => !!e && isSCMResource(e)) + // .on(this.pin, this)); + + // this._register(this.tree.onContextMenu(this.onListContextMenu, this)); + this._register(this.tree); + + // this.tree.setInput(this.repository); + + // this._register(this.viewModel.onDidChangeVisibility(this.onDidChangeVisibility, this)); + // this.onDidChangeVisibility(this.viewModel.isVisible()); + this.onDidChangeVisibility(); + this.onDidChangeBodyVisibility(visible => this.inputBox.setEnabled(visible)); + } + + private onDidChangeVisibility(): void { + // if (visible) { + const listSplicer = new ResourceGroupSplicer(this.repository.provider.groups, this.tree); + this.visibilityDisposables.push(listSplicer); + // } else { + // this.cachedScrollTop = this.tree.scrollTop; + // this.visibilityDisposables = dispose(this.visibilityDisposables); + // } + } + + layoutBody(height: number | undefined = this.cachedHeight, width: number | undefined = this.cachedWidth): void { + if (height === undefined) { + return; + } + + this.cachedHeight = height; + + if (this.repository.input.visible) { + removeClass(this.inputBoxContainer, 'hidden'); + this.inputBox.layout(); + + const editorHeight = this.inputBox.height; + const listHeight = height - (editorHeight + 12 /* margin */); + this.listContainer.style.height = `${listHeight}px`; + this.tree.layout(listHeight, width); + } else { + addClass(this.inputBoxContainer, 'hidden'); + + this.listContainer.style.height = `${height}px`; + this.tree.layout(height, width); + } + + if (this.cachedScrollTop !== undefined && this.tree.scrollTop !== this.cachedScrollTop) { + this.tree.scrollTop = Math.min(this.cachedScrollTop, this.tree.scrollHeight); + // Applying the cached scroll position just once until the next leave. + // This, also, avoids the scrollbar to flicker when resizing the sidebar. + this.cachedScrollTop = undefined; + } + } + + focus(): void { + super.focus(); + + if (this.isExpanded()) { + if (this.repository.input.visible) { + this.inputBox.focus(); + } else { + this.tree.domFocus(); + } + + this.repository.focus(); + } + } + + getActions(): IAction[] { + return this.menus.getTitleActions(); + } + + getSecondaryActions(): IAction[] { + return this.menus.getTitleSecondaryActions(); + } + + getActionViewItem(action: IAction): IActionViewItem | undefined { + if (!(action instanceof MenuItemAction)) { + return undefined; + } + + return new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + } + + getActionsContext(): any { + return this.repository.provider; + } + + private open(e: ISCMResource): void { + e.open(); + } + + // private pin(): void { + // const activeControl = this.editorService.activeControl; + // if (activeControl) { + // activeControl.group.pinEditor(activeControl.input); + // } + // } + + // private onListContextMenu(e: IListContextMenuEvent): void { + // if (!e.element) { + // return; + // } + + // const element = e.element; + // let actions: IAction[]; + + // if (isSCMResource(element)) { + // actions = this.menus.getResourceContextActions(element); + // } else { + // actions = this.menus.getResourceGroupContextActions(element); + // } + + // this.contextMenuService.showContextMenu({ + // getAnchor: () => e.anchor, + // getActions: () => actions, + // getActionsContext: () => element, + // actionRunner: new MultipleSelectionActionRunner(() => this.getSelectedResources()) + // }); + // } + + private getSelectedResources(): ISCMResource[] { + return this.tree.getSelection() + .filter(r => !!r && !isBranchNode(r) && isSCMResource(r)) as ISCMResource[]; + } + + private updateInputBox(): void { + if (typeof this.repository.provider.commitTemplate === 'undefined' || !this.repository.input.visible || this.inputBox.value) { + return; + } + + this.inputBox.value = this.repository.provider.commitTemplate; + } + + private updateInputBoxVisibility(): void { + if (this.cachedHeight) { + this.layoutBody(this.cachedHeight); + } + } + + dispose(): void { + this.visibilityDisposables = dispose(this.visibilityDisposables); + super.dispose(); + } +} + +export class RepositoryViewDescriptor implements IViewDescriptor { + + private static counter = 0; + + readonly id: string; + readonly name: string; + readonly ctorDescriptor: { ctor: any, arguments?: any[] }; + readonly canToggleVisibility = true; + readonly order = -500; + readonly workspace = true; + + constructor(readonly repository: ISCMRepository, readonly hideByDefault: boolean) { + const repoId = repository.provider.rootUri ? repository.provider.rootUri.toString() : `#${RepositoryViewDescriptor.counter++}`; + this.id = `scm:repository:${repository.provider.label}:${repoId}`; + this.name = repository.provider.rootUri ? basename(repository.provider.rootUri) : repository.provider.label; + + this.ctorDescriptor = { ctor: RepositoryPanel, arguments: [repository] }; + } +} diff --git a/src/vs/workbench/contrib/scm/browser/scmUtil.ts b/src/vs/workbench/contrib/scm/browser/scmUtil.ts deleted file mode 100644 index ec64c295f21..00000000000 --- a/src/vs/workbench/contrib/scm/browser/scmUtil.ts +++ /dev/null @@ -1,18 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ISCMResource, ISCMRepository, ISCMResourceGroup } from 'vs/workbench/contrib/scm/common/scm'; - -export function isSCMRepository(element: any): element is ISCMRepository { - return !!(element as ISCMRepository).provider && typeof (element as ISCMRepository).setSelected === 'function'; -} - -export function isSCMResourceGroup(element: any): element is ISCMResourceGroup { - return !!(element as ISCMResourceGroup).provider && !!(element as ISCMResourceGroup).elements; -} - -export function isSCMResource(element: any): element is ISCMResource { - return !!(element as ISCMResource).sourceUri && isSCMResourceGroup((element as ISCMResource).resourceGroup); -} diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index 8ac0a9e0710..35ebbf9ccad 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -6,59 +6,31 @@ import 'vs/css!./media/scmViewlet'; import { localize } from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; -import { domEvent } from 'vs/base/browser/event'; -import { basename, relativePath } from 'vs/base/common/resources'; -import { IDisposable, dispose, Disposable, DisposableStore, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; -import { append, $, addClass, toggleClass, trackFocus, removeClass, addClasses } from 'vs/base/browser/dom'; +import { append, $, toggleClass, addClasses } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { List } from 'vs/base/browser/ui/list/listWidget'; -import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent, IListEvent, IKeyboardNavigationLabelProvider, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { VIEWLET_ID, ISCMService, ISCMRepository, ISCMResourceGroup, ISCMResource, InputValidationType, VIEW_CONTAINER } 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'; +import { VIEWLET_ID, ISCMService, ISCMRepository, VIEW_CONTAINER } from 'vs/workbench/contrib/scm/common/scm'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IContextKeyService, ContextKeyExpr, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { MenuItemAction, IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; -import { IAction, Action, IActionViewItem, ActionRunner } from 'vs/base/common/actions'; -import { createAndFillInContextMenuActions, ContextAwareMenuEntryActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { SCMMenus } from './scmMenus'; -import { ActionBar, IActionViewItemProvider, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IThemeService, LIGHT } from 'vs/platform/theme/common/themeService'; -import { isSCMResource, isSCMResourceGroup } from './scmUtil'; -import { attachBadgeStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler'; +import { MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IAction, IActionViewItem } from 'vs/base/common/actions'; +import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { SCMMenus } from './menus'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; -import { Command } from 'vs/editor/common/modes'; -import { renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; -import { format } from 'vs/base/common/strings'; -import { equals } from 'vs/base/common/arrays'; -import { WorkbenchList, WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ThrottledDelayer } from 'vs/base/common/async'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import * as platform from 'vs/base/common/platform'; import { ViewContainerViewlet } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IViewsRegistry, IViewDescriptor, Extensions } from 'vs/workbench/common/views'; +import { IViewsRegistry, Extensions } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { nextTick } from 'vs/base/common/process'; -import { ITreeNode, ITreeFilter, ITreeSorter } from 'vs/base/browser/ui/tree/tree'; -import { ISequence, ISplice } from 'vs/base/common/sequence'; -import { ResourceTree, IBranchNode, isBranchNode, INode } from 'vs/base/common/resourceTree'; -import { ObjectTree, ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; -import { Iterator } from 'vs/base/common/iterator'; -import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; -import { URI } from 'vs/base/common/uri'; -import { FileKind } from 'vs/platform/files/common/files'; -import { compareFileNames } from 'vs/base/common/comparers'; -import { FuzzyScore, createMatches } from 'vs/base/common/filters'; +import { RepositoryPanel, RepositoryViewDescriptor } from 'vs/workbench/contrib/scm/browser/repositoryPanel'; +import { MainPanelDescriptor, MainPanel } from 'vs/workbench/contrib/scm/browser/mainPanel'; export interface ISpliceEvent { index: number; @@ -78,1110 +50,6 @@ export interface IViewModel { readonly onDidChangeVisibility: Event; } -class ProvidersListDelegate implements IListVirtualDelegate { - - getHeight(element: ISCMRepository): number { - return 22; - } - - getTemplateId(element: ISCMRepository): string { - return 'provider'; - } -} - -class StatusBarAction extends Action { - - constructor( - private command: Command, - private commandService: ICommandService - ) { - super(`statusbaraction{${command.id}}`, command.title, '', true); - this.tooltip = command.tooltip || ''; - } - - run(): Promise { - return this.commandService.executeCommand(this.command.id, ...(this.command.arguments || [])); - } -} - -class StatusBarActionViewItem extends ActionViewItem { - - constructor(action: StatusBarAction) { - super(null, action, {}); - } - - updateLabel(): void { - if (this.options.label) { - this.label.innerHTML = renderOcticons(this.getAction().label); - } - } -} - -function connectPrimaryMenuToInlineActionBar(menu: IMenu, actionBar: ActionBar): IDisposable { - let cachedDisposable: IDisposable = Disposable.None; - let cachedPrimary: IAction[] = []; - - const updateActions = () => { - const primary: IAction[] = []; - const secondary: IAction[] = []; - - const disposable = createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, { primary, secondary }, g => /^inline/.test(g)); - - if (equals(cachedPrimary, primary, (a, b) => a.id === b.id)) { - disposable.dispose(); - return; - } - - cachedDisposable = disposable; - cachedPrimary = primary; - - actionBar.clear(); - actionBar.push(primary, { icon: true, label: false }); - }; - - updateActions(); - - return combinedDisposable(menu.onDidChange(updateActions), toDisposable(() => { - cachedDisposable.dispose(); - })); -} - -interface RepositoryTemplateData { - title: HTMLElement; - type: HTMLElement; - countContainer: HTMLElement; - count: CountBadge; - actionBar: ActionBar; - disposable: IDisposable; - templateDisposable: IDisposable; -} - -class ProviderRenderer implements IListRenderer { - - readonly templateId = 'provider'; - - private _onDidRenderElement = new Emitter(); - readonly onDidRenderElement = this._onDidRenderElement.event; - - constructor( - @ICommandService protected commandService: ICommandService, - @IThemeService protected themeService: IThemeService - ) { } - - renderTemplate(container: HTMLElement): RepositoryTemplateData { - const provider = append(container, $('.scm-provider')); - const name = append(provider, $('.name')); - const title = append(name, $('span.title')); - const type = append(name, $('span.type')); - const countContainer = append(provider, $('.count')); - const count = new CountBadge(countContainer); - const badgeStyler = attachBadgeStyler(count, this.themeService); - const actionBar = new ActionBar(provider, { actionViewItemProvider: a => new StatusBarActionViewItem(a as StatusBarAction) }); - const disposable = Disposable.None; - const templateDisposable = combinedDisposable(actionBar, badgeStyler); - - return { title, type, countContainer, count, actionBar, disposable, templateDisposable }; - } - - renderElement(repository: ISCMRepository, index: number, templateData: RepositoryTemplateData): void { - templateData.disposable.dispose(); - const disposables = new DisposableStore(); - - if (repository.provider.rootUri) { - templateData.title.textContent = basename(repository.provider.rootUri); - templateData.type.textContent = repository.provider.label; - } else { - templateData.title.textContent = repository.provider.label; - templateData.type.textContent = ''; - } - - const actions: IAction[] = []; - const disposeActions = () => dispose(actions); - disposables.add({ dispose: disposeActions }); - - const update = () => { - disposeActions(); - - const commands = repository.provider.statusBarCommands || []; - actions.splice(0, actions.length, ...commands.map(c => new StatusBarAction(c, this.commandService))); - templateData.actionBar.clear(); - templateData.actionBar.push(actions); - - const count = repository.provider.count || 0; - toggleClass(templateData.countContainer, 'hidden', count === 0); - templateData.count.setCount(count); - - this._onDidRenderElement.fire(repository); - }; - - disposables.add(repository.provider.onDidChange(update, null)); - update(); - - templateData.disposable = disposables; - } - - disposeTemplate(templateData: RepositoryTemplateData): void { - templateData.disposable.dispose(); - templateData.templateDisposable.dispose(); - } -} - -export class MainPanel extends ViewletPanel { - - static readonly ID = 'scm.mainPanel'; - static readonly TITLE = localize('scm providers', "Source Control Providers"); - - private list: List; - - constructor( - protected viewModel: IViewModel, - options: IViewletPanelOptions, - @IKeybindingService protected keybindingService: IKeybindingService, - @IContextMenuService protected contextMenuService: IContextMenuService, - @ISCMService protected scmService: ISCMService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IMenuService private readonly menuService: IMenuService, - @IConfigurationService configurationService: IConfigurationService - ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService); - } - - protected renderBody(container: HTMLElement): void { - const delegate = new ProvidersListDelegate(); - const renderer = this.instantiationService.createInstance(ProviderRenderer); - const identityProvider = { getId: (r: ISCMRepository) => r.provider.id }; - - this.list = this.instantiationService.createInstance(WorkbenchList, `SCM Main`, container, delegate, [renderer], { - identityProvider, - horizontalScrolling: false - }); - - this._register(renderer.onDidRenderElement(e => this.list.updateWidth(this.viewModel.repositories.indexOf(e)), null)); - this._register(this.list.onSelectionChange(this.onListSelectionChange, this)); - this._register(this.list.onFocusChange(this.onListFocusChange, this)); - this._register(this.list.onContextMenu(this.onListContextMenu, this)); - - this._register(this.viewModel.onDidChangeVisibleRepositories(this.updateListSelection, this)); - - this._register(this.viewModel.onDidSplice(({ index, deleteCount, elements }) => this.splice(index, deleteCount, elements), null)); - this.splice(0, 0, this.viewModel.repositories); - - this._register(this.list); - - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('scm.providers.visible')) { - this.updateBodySize(); - } - })); - - this.updateListSelection(); - } - - private splice(index: number, deleteCount: number, repositories: ISCMRepository[] = []): void { - this.list.splice(index, deleteCount, repositories); - - const empty = this.list.length === 0; - toggleClass(this.element, 'empty', empty); - - this.updateBodySize(); - } - - protected layoutBody(height: number, width: number): void { - this.list.layout(height, width); - } - - private updateBodySize(): void { - const visibleCount = this.configurationService.getValue('scm.providers.visible'); - const empty = this.list.length === 0; - const size = Math.min(this.viewModel.repositories.length, visibleCount) * 22; - - this.minimumBodySize = visibleCount === 0 ? 22 : size; - this.maximumBodySize = visibleCount === 0 ? Number.POSITIVE_INFINITY : empty ? Number.POSITIVE_INFINITY : size; - } - - private onListContextMenu(e: IListContextMenuEvent): void { - if (!e.element) { - return; - } - - const repository = e.element; - const contextKeyService = this.contextKeyService.createScoped(); - const scmProviderKey = contextKeyService.createKey('scmProvider', undefined); - scmProviderKey.set(repository.provider.contextValue); - - const menu = this.menuService.createMenu(MenuId.SCMSourceControl, contextKeyService); - const primary: IAction[] = []; - const secondary: IAction[] = []; - const result = { primary, secondary }; - - const disposable = createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => g === 'inline'); - - menu.dispose(); - contextKeyService.dispose(); - - if (secondary.length === 0) { - return; - } - - this.contextMenuService.showContextMenu({ - getAnchor: () => e.anchor, - getActions: () => secondary, - getActionsContext: () => repository.provider - }); - - disposable.dispose(); - } - - private onListSelectionChange(e: IListEvent): void { - if (e.browserEvent && e.elements.length > 0) { - const scrollTop = this.list.scrollTop; - this.viewModel.setVisibleRepositories(e.elements); - this.list.scrollTop = scrollTop; - } - } - - private onListFocusChange(e: IListEvent): void { - if (e.browserEvent && e.elements.length > 0) { - e.elements[0].focus(); - } - } - - private updateListSelection(): void { - const set = new Set(); - - for (const repository of this.viewModel.visibleRepositories) { - set.add(repository); - } - - const selection: number[] = []; - - for (let i = 0; i < this.list.length; i++) { - if (set.has(this.list.element(i))) { - selection.push(i); - } - } - - this.list.setSelection(selection); - - if (selection.length > 0) { - this.list.setFocus([selection[0]]); - } - } -} - -interface ResourceGroupTemplate { - name: HTMLElement; - count: CountBadge; - actionBar: ActionBar; - elementDisposable: IDisposable; - dispose: () => void; -} - -class ResourceGroupRenderer implements ICompressibleTreeRenderer { - - static TEMPLATE_ID = 'resource group'; - get templateId(): string { return ResourceGroupRenderer.TEMPLATE_ID; } - - constructor( - private actionViewItemProvider: IActionViewItemProvider, - private themeService: IThemeService, - private menus: SCMMenus - ) { } - - renderTemplate(container: HTMLElement): ResourceGroupTemplate { - const element = append(container, $('.resource-group')); - const name = append(element, $('.name')); - const actionsContainer = append(element, $('.actions')); - const actionBar = new ActionBar(actionsContainer, { actionViewItemProvider: this.actionViewItemProvider }); - const countContainer = append(element, $('.count')); - const count = new CountBadge(countContainer); - const styler = attachBadgeStyler(count, this.themeService); - const elementDisposable = Disposable.None; - - return { - name, count, actionBar, elementDisposable, dispose: () => { - actionBar.dispose(); - styler.dispose(); - } - }; - } - - renderElement(node: ITreeNode, index: number, template: ResourceGroupTemplate): void { - template.elementDisposable.dispose(); - - const group = node.element; - template.name.textContent = group.label; - template.actionBar.clear(); - template.actionBar.context = group; - - const disposables = new DisposableStore(); - disposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceGroupMenu(group), template.actionBar)); - - const updateCount = () => template.count.setCount(group.elements.length); - disposables.add(group.onDidSplice(updateCount, null)); - updateCount(); - - template.elementDisposable = disposables; - } - - renderCompressedElements(node: ITreeNode, FuzzyScore>, index: number, templateData: ResourceGroupTemplate, height: number | undefined): void { - throw new Error('Should never happen since node is incompressible'); - } - - disposeElement(group: ITreeNode, index: number, template: ResourceGroupTemplate): void { - template.elementDisposable.dispose(); - } - - disposeTemplate(template: ResourceGroupTemplate): void { - template.dispose(); - } -} - -interface ResourceTemplate { - element: HTMLElement; - name: HTMLElement; - fileLabel: IResourceLabel; - decorationIcon: HTMLElement; - actionBar: ActionBar; - elementDisposable: IDisposable; - dispose: () => void; -} - -class MultipleSelectionActionRunner extends ActionRunner { - - constructor(private getSelectedResources: () => ISCMResource[]) { - super(); - } - - runAction(action: IAction, context: ISCMResource): Promise { - if (action instanceof MenuItemAction) { - const selection = this.getSelectedResources(); - const filteredSelection = selection.filter(s => s !== context); - - if (selection.length === filteredSelection.length || selection.length === 1) { - return action.run(context); - } - - return action.run(context, ...filteredSelection); - } - - return super.runAction(action, context); - } -} - -class ResourceRenderer implements ICompressibleTreeRenderer, FuzzyScore, ResourceTemplate> { - - static TEMPLATE_ID = 'resource'; - get templateId(): string { return ResourceRenderer.TEMPLATE_ID; } - - constructor( - private labels: ResourceLabels, - private actionViewItemProvider: IActionViewItemProvider, - private getSelectedResources: () => ISCMResource[], - private themeService: IThemeService, - private menus: SCMMenus - ) { } - - renderTemplate(container: HTMLElement): ResourceTemplate { - const element = append(container, $('.resource')); - const name = append(element, $('.name')); - const fileLabel = this.labels.create(name, { supportHighlights: true }); - const actionsContainer = append(fileLabel.element, $('.actions')); - const actionBar = new ActionBar(actionsContainer, { - actionViewItemProvider: this.actionViewItemProvider, - actionRunner: new MultipleSelectionActionRunner(this.getSelectedResources) - }); - - const decorationIcon = append(element, $('.decoration-icon')); - - return { - element, name, fileLabel, decorationIcon, actionBar, elementDisposable: Disposable.None, dispose: () => { - actionBar.dispose(); - fileLabel.dispose(); - } - }; - } - - renderElement(node: ITreeNode | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { - template.elementDisposable.dispose(); - - const resource = node.element; - const theme = this.themeService.getTheme(); - const icon = isBranchNode(resource) ? undefined : (theme.type === LIGHT ? resource.decorations.icon : resource.decorations.iconDark); - - const uri = isBranchNode(resource) ? URI.file(resource.path) : resource.sourceUri; - const fileKind = isBranchNode(resource) ? FileKind.FOLDER : FileKind.FILE; - template.fileLabel.setFile(uri, { - fileDecorations: { colors: false, badges: !icon }, - hidePath: true, - fileKind, - matches: createMatches(node.filterData) - }); - template.actionBar.clear(); - template.actionBar.context = resource; - - const disposables = new DisposableStore(); - - if (!isBranchNode(resource)) { - disposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceMenu(resource.resourceGroup), template.actionBar)); - toggleClass(template.name, 'strike-through', resource.decorations.strikeThrough); - toggleClass(template.element, 'faded', resource.decorations.faded); - } - - const tooltip = (isBranchNode(resource) ? resource.path : resource.decorations.tooltip) || ''; - - if (icon) { - template.decorationIcon.style.display = ''; - template.decorationIcon.style.backgroundImage = `url('${icon}')`; - template.decorationIcon.title = tooltip; - } else { - template.decorationIcon.style.display = 'none'; - template.decorationIcon.style.backgroundImage = ''; - } - - template.element.setAttribute('data-tooltip', tooltip); - template.elementDisposable = disposables; - } - - renderCompressedElements(node: ITreeNode | ICompressedTreeNode>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { - template.elementDisposable.dispose(); - - const compressed = node.element as ICompressedTreeNode>; - const resource = compressed.elements[compressed.elements.length - 1]; - - const label = compressed.elements.map(e => e.name).join('/'); - const uri = URI.file(resource.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.decorationIcon.style.display = 'none'; - template.decorationIcon.style.backgroundImage = ''; - - template.element.setAttribute('data-tooltip', resource.path); - template.elementDisposable = disposables; - - } - - disposeElement(resource: ITreeNode | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { - template.elementDisposable.dispose(); - } - - disposeTemplate(template: ResourceTemplate): void { - template.elementDisposable.dispose(); - template.dispose(); - } -} - -class ProviderListDelegate implements IListVirtualDelegate { - - getHeight() { return 22; } - - getTemplateId(element: TreeElement) { - if (isBranchNode(element) || isSCMResource(element)) { - return ResourceRenderer.TEMPLATE_ID; - } else { - return ResourceGroupRenderer.TEMPLATE_ID; - } - } -} - -class SCMTreeFilter implements ITreeFilter { - - filter(element: TreeElement): boolean { - if (isBranchNode(element)) { - return true; - } else if (isSCMResourceGroup(element)) { - return element.elements.length > 0 || !element.hideWhenEmpty; - } else { - return true; - } - } -} - -export class SCMTreeSorter implements ITreeSorter { - - compare(one: TreeElement, other: TreeElement): number { - if (isSCMResourceGroup(one) && isSCMResourceGroup(other)) { - return 0; - } - - const oneIsDirectory = isBranchNode(one); - const otherIsDirectory = isBranchNode(other); - - if (oneIsDirectory !== otherIsDirectory) { - return oneIsDirectory ? -1 : 1; - } - - const oneName = isBranchNode(one) ? one.name : basename((one as ISCMResource).sourceUri); - const otherName = isBranchNode(other) ? other.name : basename((other as ISCMResource).sourceUri); - - return compareFileNames(oneName, otherName); - } -} - -export class SCMTreeKeyboardNavigationLabelProvider implements IKeyboardNavigationLabelProvider { - - getKeyboardNavigationLabel(element: TreeElement): { toString(): string; } | undefined { - if (isSCMResourceGroup(element)) { - return element.label; - } - - if (isSCMResource(element)) { - return basename(element.sourceUri); - } - - return ''; - } -} - -const scmResourceIdentityProvider = new class implements IIdentityProvider { - getId(e: TreeElement): string { - if (isBranchNode(e)) { - return e.path; - } else if (isSCMResource(e)) { - const group = e.resourceGroup; - const provider = group.provider; - return `${provider.contextValue}/${group.id}/${e.sourceUri.toString()}`; - } else { - const provider = e.provider; - return `${provider.contextValue}/${e.id}`; - } - } -}; - -// function isGroupVisible(group: ISCMResourceGroup) { -// return group.elements.length > 0 || !group.hideWhenEmpty; -// } - -interface IGroupItem { - readonly group: ISCMResourceGroup; - readonly resources: ISCMResource[]; - readonly tree: ResourceTree; - // visible: boolean; - readonly disposable: IDisposable; -} - -function asTreeElement(node: INode, incompressible: boolean): ICompressedTreeElement { - if (isBranchNode(node)) { - return { - element: node, - children: Iterator.map(node.children, node => asTreeElement(node, false)), - incompressible, - collapsed: false - }; - } - - return { element: node.element, incompressible: true }; -} - -class ResourceGroupSplicer { - - private flat = true; - private items: IGroupItem[] = []; - private disposables = new DisposableStore(); - - constructor( - groupSequence: ISequence, - private tree: ObjectTree - ) { - groupSequence.onDidSplice(this.onDidSpliceGroups, this, this.disposables); - this.onDidSpliceGroups({ start: 0, deleteCount: 0, toInsert: groupSequence.elements }); - } - - // TODO@joao: optimize - private fullRefresh(): void { - if (this.flat) { - this.tree.setChildren(null, this.items.map(item => ({ - element: item.group, - children: Iterator.map(Iterator.fromArray(item.resources), element => ({ element, incompressible: true })), - incompressible: true - }))); - } else { - this.tree.setChildren(null, this.items.map(item => ({ - element: item.group, - children: Iterator.map(item.tree.root.children, node => asTreeElement(node, true)), - incompressible: true - }))); - } - } - - private onDidSpliceGroups({ start, deleteCount, toInsert }: ISplice): void { - const itemsToInsert: IGroupItem[] = []; - - for (const group of toInsert) { - const tree = new ResourceTree(); - const resources: ISCMResource[] = [...group.elements]; - const disposable = combinedDisposable( - group.onDidChange(() => this.onDidChangeGroup(group)), - group.onDidSplice(splice => this.onDidSpliceGroup(item, splice)) - ); - const item = { group, resources, tree, disposable }; - - itemsToInsert.push(item); - } - - const itemsToDispose = this.items.splice(start, deleteCount, ...itemsToInsert); - - for (const item of itemsToDispose) { - item.disposable.dispose(); - } - - this.fullRefresh(); - } - - private onDidChangeGroup(group: ISCMResourceGroup): void { - this.fullRefresh(); - // const itemIndex = firstIndex(this.items, item => item.group === group); - - // if (itemIndex < 0) { - // return; - // } - - // const item = this.items[itemIndex]; - // const visible = isGroupVisible(group); - - // if (item.visible === visible) { - // return; - // } - - // let absoluteStart = 0; - - // for (let i = 0; i < itemIndex; i++) { - // const item = this.items[i]; - // absoluteStart += (item.visible ? 1 : 0) + item.group.elements.length; - // } - - // if (visible) { - // this.spliceable.splice(absoluteStart, 0, [group, ...group.elements]); - // } else { - // this.spliceable.splice(absoluteStart, 1 + group.elements.length, []); - // } - - // item.visible = visible; - } - - private onDidSpliceGroup(item: IGroupItem, { start, deleteCount, toInsert }: ISplice): void { - const root = item.group.provider.rootUri || URI.file('/'); - - for (const resource of toInsert) { - item.tree.add(relativePath(root, resource.sourceUri) || resource.sourceUri.fsPath, resource); - } - - const deleted = item.resources.splice(start, deleteCount, ...toInsert); - - for (const resource of deleted) { - item.tree.delete(relativePath(root, resource.sourceUri) || resource.sourceUri.fsPath); - } - - this.fullRefresh(); - // const itemIndex = firstIndex(this.items, item => item.group === group); - - // if (itemIndex < 0) { - // return; - // } - - // const item = this.items[itemIndex]; - // const visible = isGroupVisible(group); - - // if (!item.visible && !visible) { - // return; - // } - - // let absoluteStart = start; - - // for (let i = 0; i < itemIndex; i++) { - // const item = this.items[i]; - // absoluteStart += (item.visible ? 1 : 0) + item.group.elements.length; - // } - - // if (item.visible && !visible) { - // this.spliceable.splice(absoluteStart, 1 + deleteCount, toInsert); - // } else if (!item.visible && visible) { - // this.spliceable.splice(absoluteStart, deleteCount, [group, ...toInsert]); - // } else { - // this.spliceable.splice(absoluteStart + 1, deleteCount, toInsert); - // } - - // item.visible = visible; - - - } - - dispose(): void { - this.onDidSpliceGroups({ start: 0, deleteCount: this.items.length, toInsert: [] }); - this.disposables = dispose(this.disposables); - } -} - -function convertValidationType(type: InputValidationType): MessageType { - switch (type) { - case InputValidationType.Information: return MessageType.INFO; - case InputValidationType.Warning: return MessageType.WARNING; - case InputValidationType.Error: return MessageType.ERROR; - } -} - -type TreeElement = ISCMResourceGroup | IBranchNode | ISCMResource; - -export class RepositoryPanel extends ViewletPanel { - - private cachedHeight: number | undefined = undefined; - private cachedWidth: number | undefined = undefined; - private cachedScrollTop: number | undefined = undefined; - private inputBoxContainer: HTMLElement; - private inputBox: InputBox; - private listContainer: HTMLElement; - private tree: ObjectTree; - private listLabels: ResourceLabels; - private menus: SCMMenus; - private visibilityDisposables: IDisposable[] = []; - protected contextKeyService: IContextKeyService; - - constructor( - readonly repository: ISCMRepository, - private readonly viewModel: IViewModel, - options: IViewletPanelOptions, - @IKeybindingService protected keybindingService: IKeybindingService, - @IThemeService protected themeService: IThemeService, - @IContextMenuService protected contextMenuService: IContextMenuService, - @IContextViewService protected contextViewService: IContextViewService, - @ICommandService protected commandService: ICommandService, - @INotificationService private readonly notificationService: INotificationService, - @IEditorService protected editorService: IEditorService, - @IInstantiationService protected instantiationService: IInstantiationService, - @IConfigurationService protected configurationService: IConfigurationService, - @IContextKeyService contextKeyService: IContextKeyService, - @IMenuService protected menuService: IMenuService - ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService); - - this.menus = instantiationService.createInstance(SCMMenus, this.repository.provider); - this._register(this.menus); - this._register(this.menus.onDidChangeTitle(this._onDidChangeTitleArea.fire, this._onDidChangeTitleArea)); - - this.contextKeyService = contextKeyService.createScoped(this.element); - this.contextKeyService.createKey('scmRepository', this.repository); - } - - render(): void { - super.render(); - this._register(this.menus.onDidChangeTitle(this.updateActions, this)); - } - - protected renderHeaderTitle(container: HTMLElement): void { - let title: string; - let type: string; - - if (this.repository.provider.rootUri) { - title = basename(this.repository.provider.rootUri); - type = this.repository.provider.label; - } else { - title = this.repository.provider.label; - type = ''; - } - - super.renderHeaderTitle(container, title); - addClass(container, 'scm-provider'); - append(container, $('span.type', undefined, type)); - } - - protected renderBody(container: HTMLElement): void { - const focusTracker = trackFocus(container); - this._register(focusTracker.onDidFocus(() => this.repository.focus())); - this._register(focusTracker); - - // Input - this.inputBoxContainer = append(container, $('.scm-editor')); - - const updatePlaceholder = () => { - const binding = this.keybindingService.lookupKeybinding('scm.acceptInput'); - const label = binding ? binding.getLabel() : (platform.isMacintosh ? 'Cmd+Enter' : 'Ctrl+Enter'); - const placeholder = format(this.repository.input.placeholder, label); - - this.inputBox.setPlaceHolder(placeholder); - }; - - const validationDelayer = new ThrottledDelayer(200); - const validate = () => { - return this.repository.input.validateInput(this.inputBox.value, this.inputBox.inputElement.selectionStart || 0).then(result => { - if (!result) { - this.inputBox.inputElement.removeAttribute('aria-invalid'); - this.inputBox.hideMessage(); - } else { - this.inputBox.inputElement.setAttribute('aria-invalid', 'true'); - this.inputBox.showMessage({ content: result.message, type: convertValidationType(result.type) }); - } - }); - }; - - const triggerValidation = () => validationDelayer.trigger(validate); - - this.inputBox = new InputBox(this.inputBoxContainer, this.contextViewService, { flexibleHeight: true, flexibleMaxHeight: 134 }); - this.inputBox.setEnabled(this.isBodyVisible()); - this._register(attachInputBoxStyler(this.inputBox, this.themeService)); - this._register(this.inputBox); - - this._register(this.inputBox.onDidChange(triggerValidation, null)); - - const onKeyUp = domEvent(this.inputBox.inputElement, 'keyup'); - const onMouseUp = domEvent(this.inputBox.inputElement, 'mouseup'); - this._register(Event.any(onKeyUp, onMouseUp)(triggerValidation, null)); - - this.inputBox.value = this.repository.input.value; - this._register(this.inputBox.onDidChange(value => this.repository.input.value = value, null)); - this._register(this.repository.input.onDidChange(value => this.inputBox.value = value, null)); - - updatePlaceholder(); - this._register(this.repository.input.onDidChangePlaceholder(updatePlaceholder, null)); - this._register(this.keybindingService.onDidUpdateKeybindings(updatePlaceholder, null)); - - this._register(this.inputBox.onDidHeightChange(() => this.layoutBody())); - - if (this.repository.provider.onDidChangeCommitTemplate) { - this._register(this.repository.provider.onDidChangeCommitTemplate(this.updateInputBox, this)); - } - - this.updateInputBox(); - - // Input box visibility - this._register(this.repository.input.onDidChangeVisibility(this.updateInputBoxVisibility, this)); - this.updateInputBoxVisibility(); - - // List - this.listContainer = append(container, $('.scm-status.show-file-icons')); - - const updateActionsVisibility = () => toggleClass(this.listContainer, 'show-actions', this.configurationService.getValue('scm.alwaysShowActions')); - Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowActions'))(updateActionsVisibility); - updateActionsVisibility(); - - const delegate = new ProviderListDelegate(); - - const actionViewItemProvider = (action: IAction) => this.getActionViewItem(action); - - this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); - this._register(this.listLabels); - - const renderers = [ - new ResourceGroupRenderer(actionViewItemProvider, this.themeService, this.menus), - new ResourceRenderer(this.listLabels, actionViewItemProvider, () => this.getSelectedResources(), this.themeService, this.menus) - ]; - - const filter = new SCMTreeFilter(); - const sorter = new SCMTreeSorter(); - const keyboardNavigationLabelProvider = new SCMTreeKeyboardNavigationLabelProvider(); - - this.tree = this.instantiationService.createInstance( - WorkbenchCompressibleObjectTree, - `SCM Tree Repo`, - this.listContainer, - delegate, - renderers, - { - identityProvider: scmResourceIdentityProvider, - horizontalScrolling: false, - filter, - sorter, - keyboardNavigationLabelProvider - }); - - this._register(Event.chain(this.tree.onDidOpen) - .map(e => e.elements[0]) - .filter(e => !!e && !isBranchNode(e) && isSCMResource(e)) - .on(this.open, this)); - - // this._register(Event.chain(this.tree.onPin) - // .map(e => e.elements[0]) - // .filter(e => !!e && isSCMResource(e)) - // .on(this.pin, this)); - - // this._register(this.tree.onContextMenu(this.onListContextMenu, this)); - this._register(this.tree); - - // this.tree.setInput(this.repository); - - // this._register(this.viewModel.onDidChangeVisibility(this.onDidChangeVisibility, this)); - // this.onDidChangeVisibility(this.viewModel.isVisible()); - this.onDidChangeVisibility(true); - this.onDidChangeBodyVisibility(visible => this.inputBox.setEnabled(visible)); - } - - private onDidChangeVisibility(visible: boolean): void { - // if (visible) { - const listSplicer = new ResourceGroupSplicer(this.repository.provider.groups, this.tree); - this.visibilityDisposables.push(listSplicer); - // } else { - // this.cachedScrollTop = this.tree.scrollTop; - // this.visibilityDisposables = dispose(this.visibilityDisposables); - // } - } - - layoutBody(height: number | undefined = this.cachedHeight, width: number | undefined = this.cachedWidth): void { - if (height === undefined) { - return; - } - - this.cachedHeight = height; - - if (this.repository.input.visible) { - removeClass(this.inputBoxContainer, 'hidden'); - this.inputBox.layout(); - - const editorHeight = this.inputBox.height; - const listHeight = height - (editorHeight + 12 /* margin */); - this.listContainer.style.height = `${listHeight}px`; - this.tree.layout(listHeight, width); - } else { - addClass(this.inputBoxContainer, 'hidden'); - - this.listContainer.style.height = `${height}px`; - this.tree.layout(height, width); - } - - if (this.cachedScrollTop !== undefined && this.tree.scrollTop !== this.cachedScrollTop) { - this.tree.scrollTop = Math.min(this.cachedScrollTop, this.tree.scrollHeight); - // Applying the cached scroll position just once until the next leave. - // This, also, avoids the scrollbar to flicker when resizing the sidebar. - this.cachedScrollTop = undefined; - } - } - - focus(): void { - super.focus(); - - if (this.isExpanded()) { - if (this.repository.input.visible) { - this.inputBox.focus(); - } else { - this.tree.domFocus(); - } - - this.repository.focus(); - } - } - - getActions(): IAction[] { - return this.menus.getTitleActions(); - } - - getSecondaryActions(): IAction[] { - return this.menus.getTitleSecondaryActions(); - } - - getActionViewItem(action: IAction): IActionViewItem | undefined { - if (!(action instanceof MenuItemAction)) { - return undefined; - } - - return new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); - } - - getActionsContext(): any { - return this.repository.provider; - } - - private open(e: ISCMResource): void { - e.open(); - } - - // private pin(): void { - // const activeControl = this.editorService.activeControl; - // if (activeControl) { - // activeControl.group.pinEditor(activeControl.input); - // } - // } - - // private onListContextMenu(e: IListContextMenuEvent): void { - // if (!e.element) { - // return; - // } - - // const element = e.element; - // let actions: IAction[]; - - // if (isSCMResource(element)) { - // actions = this.menus.getResourceContextActions(element); - // } else { - // actions = this.menus.getResourceGroupContextActions(element); - // } - - // this.contextMenuService.showContextMenu({ - // getAnchor: () => e.anchor, - // getActions: () => actions, - // getActionsContext: () => element, - // actionRunner: new MultipleSelectionActionRunner(() => this.getSelectedResources()) - // }); - // } - - private getSelectedResources(): ISCMResource[] { - return this.tree.getSelection() - .filter(r => !!r && !isBranchNode(r) && isSCMResource(r)) as ISCMResource[]; - } - - private updateInputBox(): void { - if (typeof this.repository.provider.commitTemplate === 'undefined' || !this.repository.input.visible || this.inputBox.value) { - return; - } - - this.inputBox.value = this.repository.provider.commitTemplate; - } - - private updateInputBoxVisibility(): void { - if (this.cachedHeight) { - this.layoutBody(this.cachedHeight); - } - } - - dispose(): void { - this.visibilityDisposables = dispose(this.visibilityDisposables); - super.dispose(); - } -} - -class RepositoryViewDescriptor implements IViewDescriptor { - - private static counter = 0; - - readonly id: string; - readonly name: string; - readonly ctorDescriptor: { ctor: any, arguments?: any[] }; - readonly canToggleVisibility = true; - readonly order = -500; - readonly workspace = true; - - constructor(readonly repository: ISCMRepository, viewModel: IViewModel, readonly hideByDefault: boolean) { - const repoId = repository.provider.rootUri ? repository.provider.rootUri.toString() : `#${RepositoryViewDescriptor.counter++}`; - this.id = `scm:repository:${repository.provider.label}:${repoId}`; - this.name = repository.provider.rootUri ? basename(repository.provider.rootUri) : repository.provider.label; - - this.ctorDescriptor = { ctor: RepositoryPanel, arguments: [repository, viewModel] }; - } -} - -class MainPanelDescriptor implements IViewDescriptor { - - readonly id = MainPanel.ID; - readonly name = MainPanel.TITLE; - readonly ctorDescriptor: { ctor: any, arguments?: any[] }; - readonly canToggleVisibility = true; - readonly hideByDefault = false; - readonly order = -1000; - readonly workspace = true; - readonly when = ContextKeyExpr.or(ContextKeyExpr.equals('config.scm.alwaysShowProviders', true), ContextKeyExpr.and(ContextKeyExpr.notEquals('scm.providerCount', 0), ContextKeyExpr.notEquals('scm.providerCount', 1))); - - constructor(viewModel: IViewModel) { - this.ctorDescriptor = { ctor: MainPanel, arguments: [viewModel] }; - } -} - export class SCMViewlet extends ViewContainerViewlet implements IViewModel { private static readonly STATE_KEY = 'workbench.scm.views.state'; @@ -1267,7 +135,7 @@ export class SCMViewlet extends ViewContainerViewlet implements IViewModel { const index = this._repositories.length; this._repositories.push(repository); - const viewDescriptor = new RepositoryViewDescriptor(repository, this, false); + const viewDescriptor = new RepositoryViewDescriptor(repository, false); Registry.as(Extensions.ViewsRegistry).registerViews([viewDescriptor], VIEW_CONTAINER); this.viewDescriptors.push(viewDescriptor); diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts new file mode 100644 index 00000000000..97a4c1eb80a --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ISCMResource, ISCMRepository, ISCMResourceGroup } from 'vs/workbench/contrib/scm/common/scm'; +import { IMenu } from 'vs/platform/actions/common/actions'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IDisposable, Disposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IAction } from 'vs/base/common/actions'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { equals } from 'vs/base/common/arrays'; + +export function isSCMRepository(element: any): element is ISCMRepository { + return !!(element as ISCMRepository).provider && typeof (element as ISCMRepository).setSelected === 'function'; +} + +export function isSCMResourceGroup(element: any): element is ISCMResourceGroup { + return !!(element as ISCMResourceGroup).provider && !!(element as ISCMResourceGroup).elements; +} + +export function isSCMResource(element: any): element is ISCMResource { + return !!(element as ISCMResource).sourceUri && isSCMResourceGroup((element as ISCMResource).resourceGroup); +} + +export function connectPrimaryMenuToInlineActionBar(menu: IMenu, actionBar: ActionBar): IDisposable { + let cachedDisposable: IDisposable = Disposable.None; + let cachedPrimary: IAction[] = []; + + const updateActions = () => { + const primary: IAction[] = []; + const secondary: IAction[] = []; + + const disposable = createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, { primary, secondary }, g => /^inline/.test(g)); + + if (equals(cachedPrimary, primary, (a, b) => a.id === b.id)) { + disposable.dispose(); + return; + } + + cachedDisposable = disposable; + cachedPrimary = primary; + + actionBar.clear(); + actionBar.push(primary, { icon: true, label: false }); + }; + + updateActions(); + + return combinedDisposable(menu.onDidChange(updateActions), toDisposable(() => { + cachedDisposable.dispose(); + })); +} From ed0f669972f8b8fd076e74b6a190e5dd84ca169b Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 17 Sep 2019 22:26:42 +0200 Subject: [PATCH 16/42] cleanup, add context menu actions back --- .../contrib/scm/browser/repositoryPanel.ts | 207 ++++++------------ 1 file changed, 73 insertions(+), 134 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index c4cbe74efc1..97743404fc3 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/scmViewlet'; import { Event } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { basename, relativePath } from 'vs/base/common/resources'; -import { IDisposable, dispose, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; import { append, $, addClass, toggleClass, trackFocus, removeClass } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IKeyboardNavigationLabelProvider, IIdentityProvider } from 'vs/base/browser/ui/list/list'; @@ -35,7 +35,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ThrottledDelayer } 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 } from 'vs/base/browser/ui/tree/tree'; +import { ITreeNode, ITreeFilter, ITreeSorter, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; import { ISequence, ISplice } from 'vs/base/common/sequence'; import { ResourceTree, IBranchNode, isBranchNode, INode } from 'vs/base/common/resourceTree'; import { ObjectTree, ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; @@ -347,6 +347,14 @@ interface IGroupItem { readonly disposable: IDisposable; } +function groupItemAsTreeElement(item: IGroupItem, flat: boolean): ICompressedTreeElement { + const children = flat + ? Iterator.map(Iterator.fromArray(item.resources), element => ({ element, incompressible: true })) + : Iterator.map(item.tree.root.children, node => asTreeElement(node, true)); + + return { element: item.group, children, incompressible: true }; +} + function asTreeElement(node: INode, incompressible: boolean): ICompressedTreeElement { if (isBranchNode(node)) { return { @@ -360,36 +368,18 @@ function asTreeElement(node: INode, incompressible: boolean): ICom return { element: node.element, incompressible: true }; } -class ResourceGroupSplicer { +// TODO: cache tree scrollTop +class ViewModel { private flat = false; private items: IGroupItem[] = []; + private visibilityDisposables = new DisposableStore(); private disposables = new DisposableStore(); constructor( - groupSequence: ISequence, + private groups: ISequence, private tree: ObjectTree - ) { - groupSequence.onDidSplice(this.onDidSpliceGroups, this, this.disposables); - this.onDidSpliceGroups({ start: 0, deleteCount: 0, toInsert: groupSequence.elements }); - } - - // TODO@joao: optimize - private fullRefresh(): void { - if (this.flat) { - this.tree.setChildren(null, this.items.map(item => ({ - element: item.group, - children: Iterator.map(Iterator.fromArray(item.resources), element => ({ element, incompressible: true })), - incompressible: true - }))); - } else { - this.tree.setChildren(null, this.items.map(item => ({ - element: item.group, - children: Iterator.map(item.tree.root.children, node => asTreeElement(node, true)), - incompressible: true - }))); - } - } + ) { } private onDidSpliceGroups({ start, deleteCount, toInsert }: ISplice): void { const itemsToInsert: IGroupItem[] = []; @@ -398,10 +388,16 @@ class ResourceGroupSplicer { const tree = new ResourceTree(); const resources: ISCMResource[] = [...group.elements]; const disposable = combinedDisposable( - group.onDidChange(() => this.onDidChangeGroup()), + group.onDidChange(() => this.tree.refilter()), group.onDidSplice(splice => this.onDidSpliceGroup(item, splice)) ); + const item = { group, resources, tree, disposable }; + const root = item.group.provider.rootUri || URI.file('/'); + + for (const resource of resources) { + item.tree.add(relativePath(root, resource.sourceUri) || resource.sourceUri.fsPath, resource); + } itemsToInsert.push(item); } @@ -412,38 +408,7 @@ class ResourceGroupSplicer { item.disposable.dispose(); } - this.fullRefresh(); - } - - private onDidChangeGroup(): void { - this.fullRefresh(); - // const itemIndex = firstIndex(this.items, item => item.group === group); - - // if (itemIndex < 0) { - // return; - // } - - // const item = this.items[itemIndex]; - // const visible = isGroupVisible(group); - - // if (item.visible === visible) { - // return; - // } - - // let absoluteStart = 0; - - // for (let i = 0; i < itemIndex; i++) { - // const item = this.items[i]; - // absoluteStart += (item.visible ? 1 : 0) + item.group.elements.length; - // } - - // if (visible) { - // this.spliceable.splice(absoluteStart, 0, [group, ...group.elements]); - // } else { - // this.spliceable.splice(absoluteStart, 1 + group.elements.length, []); - // } - - // item.visible = visible; + this.refresh(); } private onDidSpliceGroup(item: IGroupItem, { start, deleteCount, toInsert }: ISplice): void { @@ -459,43 +424,31 @@ class ResourceGroupSplicer { item.tree.delete(relativePath(root, resource.sourceUri) || resource.sourceUri.fsPath); } - this.fullRefresh(); - // const itemIndex = firstIndex(this.items, item => item.group === group); - - // if (itemIndex < 0) { - // return; - // } - - // const item = this.items[itemIndex]; - // const visible = isGroupVisible(group); - - // if (!item.visible && !visible) { - // return; - // } - - // let absoluteStart = start; - - // for (let i = 0; i < itemIndex; i++) { - // const item = this.items[i]; - // absoluteStart += (item.visible ? 1 : 0) + item.group.elements.length; - // } - - // if (item.visible && !visible) { - // this.spliceable.splice(absoluteStart, 1 + deleteCount, toInsert); - // } else if (!item.visible && visible) { - // this.spliceable.splice(absoluteStart, deleteCount, [group, ...toInsert]); - // } else { - // this.spliceable.splice(absoluteStart + 1, deleteCount, toInsert); - // } - - // item.visible = visible; + this.refresh(item); + } + 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 }); + } else { + this.visibilityDisposables.dispose(); + this.onDidSpliceGroups({ start: 0, deleteCount: this.items.length, toInsert: [] }); + } + } + private refresh(item?: IGroupItem): void { + if (item) { + this.tree.setChildren(item.group, groupItemAsTreeElement(item, this.flat).children); + } else { + this.tree.setChildren(null, this.items.map(item => groupItemAsTreeElement(item, this.flat))); + } } dispose(): void { - this.onDidSpliceGroups({ start: 0, deleteCount: this.items.length, toInsert: [] }); - this.disposables = dispose(this.disposables); + this.visibilityDisposables.dispose(); + this.disposables.dispose(); } } @@ -513,14 +466,13 @@ export class RepositoryPanel extends ViewletPanel { private cachedHeight: number | undefined = undefined; private cachedWidth: number | undefined = undefined; - private cachedScrollTop: number | undefined = undefined; private inputBoxContainer: HTMLElement; private inputBox: InputBox; private listContainer: HTMLElement; private tree: ObjectTree; + private viewModel: ViewModel; private listLabels: ResourceLabels; private menus: SCMMenus; - private visibilityDisposables: IDisposable[] = []; protected contextKeyService: IContextKeyService; constructor( @@ -679,25 +631,17 @@ export class RepositoryPanel extends ViewletPanel { // .filter(e => !!e && isSCMResource(e)) // .on(this.pin, this)); - // this._register(this.tree.onContextMenu(this.onListContextMenu, this)); + this._register(this.tree.onContextMenu(this.onListContextMenu, this)); this._register(this.tree); // this.tree.setInput(this.repository); // this._register(this.viewModel.onDidChangeVisibility(this.onDidChangeVisibility, this)); // this.onDidChangeVisibility(this.viewModel.isVisible()); - this.onDidChangeVisibility(); - this.onDidChangeBodyVisibility(visible => this.inputBox.setEnabled(visible)); - } + this.viewModel = new ViewModel(this.repository.provider.groups, this.tree); + this._register(this.viewModel); - private onDidChangeVisibility(): void { - // if (visible) { - const listSplicer = new ResourceGroupSplicer(this.repository.provider.groups, this.tree); - this.visibilityDisposables.push(listSplicer); - // } else { - // this.cachedScrollTop = this.tree.scrollTop; - // this.visibilityDisposables = dispose(this.visibilityDisposables); - // } + this._register(this.onDidChangeBodyVisibility(this._onDidChangeVisibility, this)); } layoutBody(height: number | undefined = this.cachedHeight, width: number | undefined = this.cachedWidth): void { @@ -721,13 +665,6 @@ export class RepositoryPanel extends ViewletPanel { this.listContainer.style.height = `${height}px`; this.tree.layout(height, width); } - - if (this.cachedScrollTop !== undefined && this.tree.scrollTop !== this.cachedScrollTop) { - this.tree.scrollTop = Math.min(this.cachedScrollTop, this.tree.scrollHeight); - // Applying the cached scroll position just once until the next leave. - // This, also, avoids the scrollbar to flicker when resizing the sidebar. - this.cachedScrollTop = undefined; - } } focus(): void { @@ -744,6 +681,11 @@ export class RepositoryPanel extends ViewletPanel { } } + private _onDidChangeVisibility(visible: boolean): void { + this.inputBox.setEnabled(visible); + this.viewModel.setVisible(visible); + } + getActions(): IAction[] { return this.menus.getTitleActions(); } @@ -775,27 +717,29 @@ export class RepositoryPanel extends ViewletPanel { // } // } - // private onListContextMenu(e: IListContextMenuEvent): void { - // if (!e.element) { - // return; - // } + private onListContextMenu(e: ITreeContextMenuEvent): void { + if (!e.element) { + return; + } - // const element = e.element; - // let actions: IAction[]; + const element = e.element; + let actions: IAction[]; - // if (isSCMResource(element)) { - // actions = this.menus.getResourceContextActions(element); - // } else { - // actions = this.menus.getResourceGroupContextActions(element); - // } + if (isBranchNode(element)) { + actions = []; + } else if (isSCMResource(element)) { + actions = this.menus.getResourceContextActions(element); + } else { + actions = this.menus.getResourceGroupContextActions(element); + } - // this.contextMenuService.showContextMenu({ - // getAnchor: () => e.anchor, - // getActions: () => actions, - // getActionsContext: () => element, - // actionRunner: new MultipleSelectionActionRunner(() => this.getSelectedResources()) - // }); - // } + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => actions, + getActionsContext: () => element, + actionRunner: new MultipleSelectionActionRunner(() => this.getSelectedResources()) + }); + } private getSelectedResources(): ISCMResource[] { return this.tree.getSelection() @@ -815,11 +759,6 @@ export class RepositoryPanel extends ViewletPanel { this.layoutBody(this.cachedHeight); } } - - dispose(): void { - this.visibilityDisposables = dispose(this.visibilityDisposables); - super.dispose(); - } } export class RepositoryViewDescriptor implements IViewDescriptor { From 99a3db6eaa42cfd70dec2742a6c17db1b2bf39b9 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 17 Sep 2019 22:26:49 +0200 Subject: [PATCH 17/42] :lipstick: --- src/vs/workbench/contrib/scm/browser/repositoryPanel.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index 97743404fc3..76afd13d549 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -335,10 +335,6 @@ const scmResourceIdentityProvider = new class implements IIdentityProvider 0 || !group.hideWhenEmpty; -// } - interface IGroupItem { readonly group: ISCMResourceGroup; readonly resources: ISCMResource[]; From 7e9687c115eeaa52687d9de65b9bd38ffd306fd1 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 17 Sep 2019 22:35:41 +0200 Subject: [PATCH 18/42] AbstractTree.onDidPin --- src/vs/base/browser/ui/list/listPaging.ts | 2 +- src/vs/base/browser/ui/list/listWidget.ts | 12 +++++------- src/vs/base/browser/ui/tree/abstractTree.ts | 1 + 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/vs/base/browser/ui/list/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts index cc6ff3a92bb..4d022cc8bc7 100644 --- a/src/vs/base/browser/ui/list/listPaging.ts +++ b/src/vs/base/browser/ui/list/listPaging.ts @@ -127,7 +127,7 @@ export class PagedList implements IDisposable { } get onPin(): Event> { - return Event.map(this.list.onPin, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes })); + return Event.map(this.list.onDidPin, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes })); } get onContextMenu(): Event> { diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index ab64dd8bdd4..fa3039d1078 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -1110,10 +1110,8 @@ export class List implements ISpliceable, IDisposable { private _onDidOpen = new Emitter>(); readonly onDidOpen: Event> = this._onDidOpen.event; - private _onPin = new Emitter(); - @memoize get onPin(): Event> { - return Event.map(this._onPin.event, indexes => this.toListEvent({ indexes })); - } + private _onDidPin = new Emitter>(); + readonly onDidPin: Event> = this._onDidPin.event; get domId(): string { return this.view.domId; } get onDidScroll(): Event { return this.view.onDidScroll; } @@ -1582,14 +1580,14 @@ export class List implements ISpliceable, IDisposable { this._onDidOpen.fire({ indexes, elements: indexes.map(i => this.view.element(i)), browserEvent }); } - pin(indexes: number[]): void { + pin(indexes: number[], browserEvent?: UIEvent): void { for (const index of indexes) { if (index < 0 || index >= this.length) { throw new ListError(this.user, `Invalid index ${index}`); } } - this._onPin.fire(indexes); + this._onDidPin.fire({ indexes, elements: indexes.map(i => this.view.element(i)), browserEvent }); } style(styles: IListStyles): void { @@ -1626,7 +1624,7 @@ export class List implements ISpliceable, IDisposable { this.disposables.dispose(); this._onDidOpen.dispose(); - this._onPin.dispose(); + this._onDidPin.dispose(); this._onDidDispose.dispose(); } } diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 2f739b87132..d390ba15657 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -1189,6 +1189,7 @@ export abstract class AbstractTree implements IDisposable get onDidChangeFocus(): Event> { return this.eventBufferer.wrapEvent(this.focus.onDidChange); } get onDidChangeSelection(): Event> { return this.eventBufferer.wrapEvent(this.selection.onDidChange); } get onDidOpen(): Event> { return Event.map(this.view.onDidOpen, asTreeEvent); } + get onDidPin(): Event> { return Event.map(this.view.onDidPin, asTreeEvent); } get onMouseClick(): Event> { return Event.map(this.view.onMouseClick, asTreeMouseEvent); } get onMouseDblClick(): Event> { return Event.map(this.view.onMouseDblClick, asTreeMouseEvent); } From 39e58d5244dae55bd5092c5c62b754f8548a44be Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 17 Sep 2019 22:35:48 +0200 Subject: [PATCH 19/42] scm: pin --- .../contrib/scm/browser/repositoryPanel.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index 76afd13d549..b4758322e3b 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -339,7 +339,6 @@ interface IGroupItem { readonly group: ISCMResourceGroup; readonly resources: ISCMResource[]; readonly tree: ResourceTree; - // visible: boolean; readonly disposable: IDisposable; } @@ -622,10 +621,10 @@ export class RepositoryPanel extends ViewletPanel { .filter(e => !!e && !isBranchNode(e) && isSCMResource(e)) .on(this.open, this)); - // this._register(Event.chain(this.tree.onPin) - // .map(e => e.elements[0]) - // .filter(e => !!e && isSCMResource(e)) - // .on(this.pin, this)); + this._register(Event.chain(this.tree.onDidPin) + .map(e => e.elements[0]) + .filter(e => !!e && !isBranchNode(e) && isSCMResource(e)) + .on(this.pin, this)); this._register(this.tree.onContextMenu(this.onListContextMenu, this)); this._register(this.tree); @@ -706,12 +705,13 @@ export class RepositoryPanel extends ViewletPanel { e.open(); } - // private pin(): void { - // const activeControl = this.editorService.activeControl; - // if (activeControl) { - // activeControl.group.pinEditor(activeControl.input); - // } - // } + private pin(): void { + const activeControl = this.editorService.activeControl; + + if (activeControl) { + activeControl.group.pinEditor(activeControl.input); + } + } private onListContextMenu(e: ITreeContextMenuEvent): void { if (!e.element) { From 6982e2fe235aa7edc8241d987a5267e24da88429 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 17 Sep 2019 22:38:01 +0200 Subject: [PATCH 20/42] :lipstick: --- src/vs/workbench/contrib/scm/browser/repositoryPanel.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index b4758322e3b..259050c9687 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -604,7 +604,7 @@ export class RepositoryPanel extends ViewletPanel { this.tree = this.instantiationService.createInstance( WorkbenchCompressibleObjectTree, - `SCM Tree Repo`, + 'SCM Tree Repo', this.listContainer, delegate, renderers, @@ -629,10 +629,6 @@ export class RepositoryPanel extends ViewletPanel { this._register(this.tree.onContextMenu(this.onListContextMenu, this)); this._register(this.tree); - // this.tree.setInput(this.repository); - - // this._register(this.viewModel.onDidChangeVisibility(this.onDidChangeVisibility, this)); - // this.onDidChangeVisibility(this.viewModel.isVisible()); this.viewModel = new ViewModel(this.repository.provider.groups, this.tree); this._register(this.viewModel); From 33deade905f6a4868e97134d1ac661a25cf95a3a Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 17 Sep 2019 22:39:01 +0200 Subject: [PATCH 21/42] :lipstick: --- src/vs/workbench/contrib/scm/browser/repositoryPanel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index 259050c9687..4edea5036fb 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -47,6 +47,8 @@ import { compareFileNames } from 'vs/base/common/comparers'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { IViewDescriptor } from 'vs/workbench/common/views'; +type TreeElement = ISCMResourceGroup | IBranchNode | ISCMResource; + interface ResourceGroupTemplate { name: HTMLElement; count: CountBadge; @@ -455,8 +457,6 @@ function convertValidationType(type: InputValidationType): MessageType { } } -type TreeElement = ISCMResourceGroup | IBranchNode | ISCMResource; - export class RepositoryPanel extends ViewletPanel { private cachedHeight: number | undefined = undefined; From 73357b8a90eb04361aa3dbbbdbcd5376a4e36fe3 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 17 Sep 2019 22:45:44 +0200 Subject: [PATCH 22/42] ResourceTree :lipstick: --- src/vs/base/common/resourceTree.ts | 18 ++++--- src/vs/base/test/common/resourceTree.test.ts | 21 ++++---- .../contrib/scm/browser/repositoryPanel.ts | 49 +++++++++---------- 3 files changed, 46 insertions(+), 42 deletions(-) diff --git a/src/vs/base/common/resourceTree.ts b/src/vs/base/common/resourceTree.ts index 79ea1f16c2c..5821f9ce5fe 100644 --- a/src/vs/base/common/resourceTree.ts +++ b/src/vs/base/common/resourceTree.ts @@ -6,6 +6,8 @@ import { memoize } from 'vs/base/common/decorators'; import * as paths from 'vs/base/common/path'; import { Iterator } from 'vs/base/common/iterator'; +import { relativePath } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; export interface ILeafNode { readonly path: string; @@ -23,10 +25,6 @@ export interface IBranchNode { export type INode = IBranchNode | ILeafNode; -export function isBranchNode(obj: any): obj is IBranchNode { - return obj instanceof BranchNode; -} - // Internals class Node { @@ -73,7 +71,14 @@ export class ResourceTree> { readonly root = new BranchNode(''); - add(key: string, element: T): void { + static isBranchNode(obj: any): obj is IBranchNode { + return obj instanceof BranchNode; + } + + constructor(private rootURI: URI = URI.file('/')) { } + + add(uri: URI, element: T): void { + const key = relativePath(this.rootURI, uri) || uri.fsPath; const parts = key.split(/[\\\/]/).filter(p => !!p); let node = this.root; let path = this.root.path; @@ -111,7 +116,8 @@ export class ResourceTree> { } } - delete(key: string): T | undefined { + delete(uri: URI): T | undefined { + const key = relativePath(this.rootURI, uri) || uri.fsPath; const parts = key.split(/[\\\/]/).filter(p => !!p); return this._delete(this.root, parts, 0); } diff --git a/src/vs/base/test/common/resourceTree.test.ts b/src/vs/base/test/common/resourceTree.test.ts index 97af22d260b..6bc77542499 100644 --- a/src/vs/base/test/common/resourceTree.test.ts +++ b/src/vs/base/test/common/resourceTree.test.ts @@ -4,45 +4,46 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ResourceTree, IBranchNode, ILeafNode, isBranchNode } from 'vs/base/common/resourceTree'; +import { ResourceTree, IBranchNode, ILeafNode } from 'vs/base/common/resourceTree'; +import { URI } from 'vs/base/common/uri'; suite('ResourceTree', function () { test('ctor', function () { const tree = new ResourceTree(); - assert(isBranchNode(tree.root)); + assert(ResourceTree.isBranchNode(tree.root)); assert.equal(tree.root.size, 0); }); test('simple', function () { const tree = new ResourceTree(); - tree.add('/foo/bar.txt', 'bar contents'); - assert(isBranchNode(tree.root)); + tree.add(URI.file('/foo/bar.txt'), 'bar contents'); + assert(ResourceTree.isBranchNode(tree.root)); assert.equal(tree.root.size, 1); let foo = tree.root.get('foo') as IBranchNode; assert(foo); - assert(isBranchNode(foo)); + assert(ResourceTree.isBranchNode(foo)); assert.equal(foo.size, 1); let bar = foo.get('bar.txt') as ILeafNode; assert(bar); - assert(!isBranchNode(bar)); + assert(!ResourceTree.isBranchNode(bar)); assert.equal(bar.element, 'bar contents'); - tree.add('/hello.txt', 'hello contents'); + tree.add(URI.file('/hello.txt'), 'hello contents'); assert.equal(tree.root.size, 2); let hello = tree.root.get('hello.txt') as ILeafNode; assert(hello); - assert(!isBranchNode(hello)); + assert(!ResourceTree.isBranchNode(hello)); assert.equal(hello.element, 'hello contents'); - tree.delete('/foo/bar.txt'); + tree.delete(URI.file('/foo/bar.txt')); assert.equal(tree.root.size, 1); hello = tree.root.get('hello.txt') as ILeafNode; assert(hello); - assert(!isBranchNode(hello)); + assert(!ResourceTree.isBranchNode(hello)); assert.equal(hello.element, 'hello contents'); }); }); diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index 4edea5036fb..4336385ea13 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/scmViewlet'; import { Event } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; -import { basename, relativePath } from 'vs/base/common/resources'; +import { basename } from 'vs/base/common/resources'; import { IDisposable, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; import { append, $, addClass, toggleClass, trackFocus, removeClass } from 'vs/base/browser/dom'; @@ -37,7 +37,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati 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 { ResourceTree, IBranchNode, isBranchNode, INode } from 'vs/base/common/resourceTree'; +import { ResourceTree, IBranchNode, INode } from 'vs/base/common/resourceTree'; import { ObjectTree, ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { Iterator } from 'vs/base/common/iterator'; import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; @@ -187,10 +187,10 @@ class ResourceRenderer implements ICompressibleTreeRenderer { getHeight() { return 22; } getTemplateId(element: TreeElement) { - if (isBranchNode(element) || isSCMResource(element)) { + if (ResourceTree.isBranchNode(element) || isSCMResource(element)) { return ResourceRenderer.TEMPLATE_ID; } else { return ResourceGroupRenderer.TEMPLATE_ID; @@ -276,7 +276,7 @@ class ProviderListDelegate implements IListVirtualDelegate { class SCMTreeFilter implements ITreeFilter { filter(element: TreeElement): boolean { - if (isBranchNode(element)) { + if (ResourceTree.isBranchNode(element)) { return true; } else if (isSCMResourceGroup(element)) { return element.elements.length > 0 || !element.hideWhenEmpty; @@ -293,15 +293,15 @@ export class SCMTreeSorter implements ITreeSorter { return 0; } - const oneIsDirectory = isBranchNode(one); - const otherIsDirectory = isBranchNode(other); + const oneIsDirectory = ResourceTree.isBranchNode(one); + const otherIsDirectory = ResourceTree.isBranchNode(other); if (oneIsDirectory !== otherIsDirectory) { return oneIsDirectory ? -1 : 1; } - const oneName = isBranchNode(one) ? one.name : basename((one as ISCMResource).sourceUri); - const otherName = isBranchNode(other) ? other.name : basename((other as ISCMResource).sourceUri); + const oneName = ResourceTree.isBranchNode(one) ? one.name : basename((one as ISCMResource).sourceUri); + const otherName = ResourceTree.isBranchNode(other) ? other.name : basename((other as ISCMResource).sourceUri); return compareFileNames(oneName, otherName); } @@ -324,7 +324,7 @@ export class SCMTreeKeyboardNavigationLabelProvider implements IKeyboardNavigati const scmResourceIdentityProvider = new class implements IIdentityProvider { getId(e: TreeElement): string { - if (isBranchNode(e)) { + if (ResourceTree.isBranchNode(e)) { return e.path; } else if (isSCMResource(e)) { const group = e.resourceGroup; @@ -353,7 +353,7 @@ function groupItemAsTreeElement(item: IGroupItem, flat: boolean): ICompressedTre } function asTreeElement(node: INode, incompressible: boolean): ICompressedTreeElement { - if (isBranchNode(node)) { + if (ResourceTree.isBranchNode(node)) { return { element: node, children: Iterator.map(node.children, node => asTreeElement(node, false)), @@ -382,7 +382,7 @@ class ViewModel { const itemsToInsert: IGroupItem[] = []; for (const group of toInsert) { - const tree = new ResourceTree(); + const tree = new ResourceTree(group.provider.rootUri || URI.file('/')); const resources: ISCMResource[] = [...group.elements]; const disposable = combinedDisposable( group.onDidChange(() => this.tree.refilter()), @@ -390,10 +390,9 @@ class ViewModel { ); const item = { group, resources, tree, disposable }; - const root = item.group.provider.rootUri || URI.file('/'); for (const resource of resources) { - item.tree.add(relativePath(root, resource.sourceUri) || resource.sourceUri.fsPath, resource); + item.tree.add(resource.sourceUri, resource); } itemsToInsert.push(item); @@ -409,16 +408,14 @@ class ViewModel { } private onDidSpliceGroup(item: IGroupItem, { start, deleteCount, toInsert }: ISplice): void { - const root = item.group.provider.rootUri || URI.file('/'); - for (const resource of toInsert) { - item.tree.add(relativePath(root, resource.sourceUri) || resource.sourceUri.fsPath, resource); + item.tree.add(resource.sourceUri, resource); } const deleted = item.resources.splice(start, deleteCount, ...toInsert); for (const resource of deleted) { - item.tree.delete(relativePath(root, resource.sourceUri) || resource.sourceUri.fsPath); + item.tree.delete(resource.sourceUri); } this.refresh(item); @@ -618,12 +615,12 @@ export class RepositoryPanel extends ViewletPanel { this._register(Event.chain(this.tree.onDidOpen) .map(e => e.elements[0]) - .filter(e => !!e && !isBranchNode(e) && isSCMResource(e)) + .filter(e => !!e && !ResourceTree.isBranchNode(e) && isSCMResource(e)) .on(this.open, this)); this._register(Event.chain(this.tree.onDidPin) .map(e => e.elements[0]) - .filter(e => !!e && !isBranchNode(e) && isSCMResource(e)) + .filter(e => !!e && !ResourceTree.isBranchNode(e) && isSCMResource(e)) .on(this.pin, this)); this._register(this.tree.onContextMenu(this.onListContextMenu, this)); @@ -717,7 +714,7 @@ export class RepositoryPanel extends ViewletPanel { const element = e.element; let actions: IAction[]; - if (isBranchNode(element)) { + if (ResourceTree.isBranchNode(element)) { actions = []; } else if (isSCMResource(element)) { actions = this.menus.getResourceContextActions(element); @@ -735,7 +732,7 @@ export class RepositoryPanel extends ViewletPanel { private getSelectedResources(): ISCMResource[] { return this.tree.getSelection() - .filter(r => !!r && !isBranchNode(r) && isSCMResource(r)) as ISCMResource[]; + .filter(r => !!r && !ResourceTree.isBranchNode(r) && isSCMResource(r)) as ISCMResource[]; } private updateInputBox(): void { From 9a60edcf0ade9889fc731066c2502e1a5e692f10 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 17 Sep 2019 22:53:36 +0200 Subject: [PATCH 23/42] fix padding --- src/vs/workbench/contrib/scm/browser/media/scmViewlet.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css index b4c0f3df1f7..0a175940e0b 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css +++ b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css @@ -33,6 +33,7 @@ align-items: center; flex-wrap: wrap; height: 100%; + padding: 0 12px 0 20px; } .scm-viewlet .monaco-list-row > .scm-provider > .monaco-action-bar { @@ -60,7 +61,6 @@ } .scm-viewlet .monaco-list-row { - /* padding: 0 12px 0 20px; */ line-height: 22px; } From 79934c980766d93a5ad149835340f1fb1e42412c Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 17 Sep 2019 23:26:02 +0200 Subject: [PATCH 24/42] scm: ToggleViewModeAction --- .../contrib/scm/browser/media/list-dark.svg | 3 + .../contrib/scm/browser/media/list-hc.svg | 3 + .../contrib/scm/browser/media/list-light.svg | 3 + .../contrib/scm/browser/media/scmViewlet.css | 24 +++++++ .../contrib/scm/browser/media/tree-dark.svg | 3 + .../contrib/scm/browser/media/tree-hc.svg | 3 + .../contrib/scm/browser/media/tree-light.svg | 3 + .../contrib/scm/browser/repositoryPanel.ts | 67 ++++++++++++++++--- 8 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 src/vs/workbench/contrib/scm/browser/media/list-dark.svg create mode 100644 src/vs/workbench/contrib/scm/browser/media/list-hc.svg create mode 100644 src/vs/workbench/contrib/scm/browser/media/list-light.svg create mode 100644 src/vs/workbench/contrib/scm/browser/media/tree-dark.svg create mode 100644 src/vs/workbench/contrib/scm/browser/media/tree-hc.svg create mode 100644 src/vs/workbench/contrib/scm/browser/media/tree-light.svg diff --git a/src/vs/workbench/contrib/scm/browser/media/list-dark.svg b/src/vs/workbench/contrib/scm/browser/media/list-dark.svg new file mode 100644 index 00000000000..eb1964b511d --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/media/list-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/workbench/contrib/scm/browser/media/list-hc.svg b/src/vs/workbench/contrib/scm/browser/media/list-hc.svg new file mode 100644 index 00000000000..d3145dd9ae7 --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/media/list-hc.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/workbench/contrib/scm/browser/media/list-light.svg b/src/vs/workbench/contrib/scm/browser/media/list-light.svg new file mode 100644 index 00000000000..32fb1216f0d --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/media/list-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css index 0a175940e0b..dd900f7dd82 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css +++ b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css @@ -7,6 +7,30 @@ -webkit-mask: url('scm-activity-bar.svg') no-repeat 50% 50%; } +.monaco-workbench .scm-action.toggle-view-mode.list { + background: url('list-light.svg') center center no-repeat; +} + +.vs-dark .monaco-workbench .scm-action.toggle-view-mode.list { + background: url('list-dark.svg') center center no-repeat; +} + +.hc-black .monaco-workbench .scm-action.toggle-view-mode.list { + background: url('list-hc.svg') center center no-repeat; +} + +.monaco-workbench .scm-action.toggle-view-mode.tree { + background: url('tree-light.svg') center center no-repeat; +} + +.vs-dark .monaco-workbench .scm-action.toggle-view-mode.tree { + background: url('tree-dark.svg') center center no-repeat; +} + +.hc-black .monaco-workbench .scm-action.toggle-view-mode.tree { + background: url('tree-hc.svg') center center no-repeat; +} + .monaco-workbench .viewlet.scm-viewlet .collapsible.header .actions { width: initial; flex: 1; diff --git a/src/vs/workbench/contrib/scm/browser/media/tree-dark.svg b/src/vs/workbench/contrib/scm/browser/media/tree-dark.svg new file mode 100644 index 00000000000..3ef43a1c0ee --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/media/tree-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/workbench/contrib/scm/browser/media/tree-hc.svg b/src/vs/workbench/contrib/scm/browser/media/tree-hc.svg new file mode 100644 index 00000000000..1739392777e --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/media/tree-hc.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/workbench/contrib/scm/browser/media/tree-light.svg b/src/vs/workbench/contrib/scm/browser/media/tree-light.svg new file mode 100644 index 00000000000..58d6cc457bb --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/media/tree-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index 4336385ea13..8e3535e3e57 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/scmViewlet'; -import { Event } from 'vs/base/common/event'; +import { Event, Emitter } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { basename } from 'vs/base/common/resources'; import { IDisposable, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; @@ -21,7 +21,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { MenuItemAction, IMenuService } from 'vs/platform/actions/common/actions'; -import { IAction, IActionViewItem, ActionRunner } from 'vs/base/common/actions'; +import { IAction, IActionViewItem, ActionRunner, Action } from 'vs/base/common/actions'; import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { SCMMenus } from './menus'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -46,6 +46,7 @@ import { FileKind } from 'vs/platform/files/common/files'; import { compareFileNames } from 'vs/base/common/comparers'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { IViewDescriptor } from 'vs/workbench/common/views'; +import { localize } from 'vs/nls'; type TreeElement = ISCMResourceGroup | IBranchNode | ISCMResource; @@ -344,8 +345,8 @@ interface IGroupItem { readonly disposable: IDisposable; } -function groupItemAsTreeElement(item: IGroupItem, flat: boolean): ICompressedTreeElement { - const children = flat +function groupItemAsTreeElement(item: IGroupItem, mode: ViewModelMode): ICompressedTreeElement { + const children = mode === ViewModelMode.List ? Iterator.map(Iterator.fromArray(item.resources), element => ({ element, incompressible: true })) : Iterator.map(item.tree.root.children, node => asTreeElement(node, true)); @@ -365,10 +366,25 @@ function asTreeElement(node: INode, incompressible: boolean): ICom return { element: node.element, incompressible: true }; } +const enum ViewModelMode { + List = 'list', + Tree = 'tree' +} + // TODO: cache tree scrollTop class ViewModel { - private flat = false; + private _mode = ViewModelMode.Tree; + private _onDidChangeMode = new Emitter(); + readonly onDidChangeMode = this._onDidChangeMode.event; + + get mode(): ViewModelMode { return this._mode; } + set mode(mode: ViewModelMode) { + this._mode = mode; + this.refresh(); + this._onDidChangeMode.fire(mode); + } + private items: IGroupItem[] = []; private visibilityDisposables = new DisposableStore(); private disposables = new DisposableStore(); @@ -434,9 +450,9 @@ class ViewModel { private refresh(item?: IGroupItem): void { if (item) { - this.tree.setChildren(item.group, groupItemAsTreeElement(item, this.flat).children); + this.tree.setChildren(item.group, groupItemAsTreeElement(item, this.mode).children); } else { - this.tree.setChildren(null, this.items.map(item => groupItemAsTreeElement(item, this.flat))); + this.tree.setChildren(null, this.items.map(item => groupItemAsTreeElement(item, this.mode))); } } @@ -446,6 +462,27 @@ class ViewModel { } } +export class ToggleViewModeAction extends Action { + + static readonly ID = 'workbench.scm.action.toggleViewMode'; + static readonly LABEL = localize('toggleViewMode', "ToggleViewMode"); + + constructor(private viewModel: ViewModel) { + super(ToggleViewModeAction.ID, ToggleViewModeAction.LABEL); + + this._register(this.viewModel.onDidChangeMode(this.onDidChangeMode, this)); + this.onDidChangeMode(this.viewModel.mode); + } + + async run(): Promise { + this.viewModel.mode = this.viewModel.mode === ViewModelMode.List ? ViewModelMode.Tree : ViewModelMode.List; + } + + private onDidChangeMode(mode: ViewModelMode): void { + this.class = `scm-action toggle-view-mode ${mode}`; + } +} + function convertValidationType(type: InputValidationType): MessageType { switch (type) { case InputValidationType.Information: return MessageType.INFO; @@ -465,6 +502,7 @@ export class RepositoryPanel extends ViewletPanel { private viewModel: ViewModel; private listLabels: ResourceLabels; private menus: SCMMenus; + private toggleViewModelModeAction: ToggleViewModeAction | undefined; protected contextKeyService: IContextKeyService; constructor( @@ -629,7 +667,12 @@ export class RepositoryPanel extends ViewletPanel { this.viewModel = new ViewModel(this.repository.provider.groups, this.tree); this._register(this.viewModel); + this.toggleViewModelModeAction = new ToggleViewModeAction(this.viewModel); + this._register(this.toggleViewModelModeAction); + this._register(this.onDidChangeBodyVisibility(this._onDidChangeVisibility, this)); + + this.updateActions(); } layoutBody(height: number | undefined = this.cachedHeight, width: number | undefined = this.cachedWidth): void { @@ -675,7 +718,15 @@ export class RepositoryPanel extends ViewletPanel { } getActions(): IAction[] { - return this.menus.getTitleActions(); + if (this.toggleViewModelModeAction) { + + return [ + this.toggleViewModelModeAction, + ...this.menus.getTitleActions() + ]; + } else { + return this.menus.getTitleActions(); + } } getSecondaryActions(): IAction[] { From 57a4fec36221869fc2325e71f2f39de973fc2212 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Tue, 17 Sep 2019 23:28:34 +0200 Subject: [PATCH 25/42] scm: render paths in list mode --- src/vs/workbench/contrib/scm/browser/repositoryPanel.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index 8e3535e3e57..6c361cd622d 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -156,6 +156,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer ViewModel, private labels: ResourceLabels, private actionViewItemProvider: IActionViewItemProvider, private getSelectedResources: () => ISCMResource[], @@ -192,9 +193,11 @@ class ResourceRenderer implements ICompressibleTreeRenderer this.getSelectedResources(), this.themeService, this.menus) + new ResourceRenderer(() => this.viewModel, this.listLabels, actionViewItemProvider, () => this.getSelectedResources(), this.themeService, this.menus) ]; const filter = new SCMTreeFilter(); From 701b16af3b5faddd6e97917d54355f55f4cb7f35 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 18 Sep 2019 08:07:40 +0200 Subject: [PATCH 26/42] fix bad ref --- src/vs/workbench/contrib/scm/browser/scm.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 84504b8b1f5..bacb6796fa8 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -13,7 +13,7 @@ import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } fro 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 { SCMStatusController } from './scmActivity'; +import { SCMStatusController } from './activity'; import { SCMViewlet } from 'vs/workbench/contrib/scm/browser/scmViewlet'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; From bcd7bcedff3844ccd3bd2b57fa5132defc0e4539 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 18 Sep 2019 09:49:35 +0200 Subject: [PATCH 27/42] wip: scm folder menus --- extensions/git/package.json | 47 ++++++++ src/vs/base/common/resourceTree.ts | 15 ++- src/vs/platform/actions/common/actions.ts | 1 + .../api/common/menusExtensionPoint.ts | 3 +- src/vs/workbench/contrib/scm/browser/menus.ts | 43 ++++---- .../contrib/scm/browser/repositoryPanel.ts | 101 +++++++++++------- 6 files changed, 150 insertions(+), 60 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index 66b8731c6d9..efa8d35b724 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -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", diff --git a/src/vs/base/common/resourceTree.ts b/src/vs/base/common/resourceTree.ts index 5821f9ce5fe..e4420c078f1 100644 --- a/src/vs/base/common/resourceTree.ts +++ b/src/vs/base/common/resourceTree.ts @@ -20,6 +20,7 @@ export interface IBranchNode { readonly name: string; readonly size: number; readonly children: Iterator>; + readonly parent: IBranchNode | undefined; get(childName: string): INode | undefined; } @@ -47,6 +48,10 @@ class BranchNode extends Node implements IBranchNode { return Iterator.fromIterableIterator(this._children.values()); } + constructor(path: string, readonly parent: IBranchNode | undefined = undefined) { + super(path); + } + get(path: string): BranchNode | LeafNode | undefined { return this._children.get(path); } @@ -75,6 +80,14 @@ export class ResourceTree> { return obj instanceof BranchNode; } + static getRoot(node: IBranchNode): IBranchNode { + 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> { 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); diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 7a409b98a5e..180b78f03e1 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -87,6 +87,7 @@ export const enum MenuId { ProblemsPanelContext, SCMChangeContext, SCMResourceContext, + SCMResourceFolderContext, SCMResourceGroupContext, SCMSourceControl, SCMTitle, diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index cb6cddcb57d..0dd6facb692 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -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; diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index 02a31f096d2..327801803c2 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -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(); - 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): void { const menuEntriesToInsert = toInsert.map(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()); } } diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index 6c361cd622d..6596fb964f8 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -54,8 +54,8 @@ interface ResourceGroupTemplate { name: HTMLElement; count: CountBadge; actionBar: ActionBar; - elementDisposable: IDisposable; - dispose: () => void; + elementDisposables: IDisposable; + disposables: IDisposable; } class ResourceGroupRenderer implements ICompressibleTreeRenderer { @@ -77,18 +77,14 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer { - actionBar.dispose(); - styler.dispose(); - } - }; + return { name, count, actionBar, elementDisposables, disposables }; } renderElement(node: ITreeNode, 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, FuzzyScore>, index: number, templateData: ResourceGroupTemplate, height: number | undefined): void { @@ -110,11 +106,11 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer, 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 { - actionBar.dispose(); - fileLabel.dispose(); - } - }; + return { element, name, fileLabel, decorationIcon, actionBar, elementDisposables: Disposable.None, disposables }; } renderElement(node: ITreeNode | ITreeNode, 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 | ICompressedTreeNode>, 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>; - 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 | ITreeNode, 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): 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 { From ced9fdfa4f8875e5a605f3db7b5edc060e86beaf Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 18 Sep 2019 11:58:44 +0200 Subject: [PATCH 28/42] scm: folder context actions --- extensions/git/package.nls.json | 2 +- src/vs/base/common/resourceTree.ts | 88 ++++++---- src/vs/base/test/common/resourceTree.test.ts | 12 +- .../contrib/scm/browser/media/scmViewlet.css | 3 + .../contrib/scm/browser/repositoryPanel.ts | 159 +++++++++--------- 5 files changed, 143 insertions(+), 121 deletions(-) diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index b4cbdad0986..6587b65421d 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -55,7 +55,7 @@ "command.syncRebase": "Sync (Rebase)", "command.publish": "Publish Branch", "command.showOutput": "Show Git Output", - "command.ignore": "Add File to .gitignore", + "command.ignore": "Add to .gitignore", "command.stashIncludeUntracked": "Stash (Include Untracked)", "command.stash": "Stash", "command.stashPop": "Pop Stash...", diff --git a/src/vs/base/common/resourceTree.ts b/src/vs/base/common/resourceTree.ts index e4420c078f1..094c49dc2e8 100644 --- a/src/vs/base/common/resourceTree.ts +++ b/src/vs/base/common/resourceTree.ts @@ -6,57 +6,61 @@ import { memoize } from 'vs/base/common/decorators'; import * as paths from 'vs/base/common/path'; import { Iterator } from 'vs/base/common/iterator'; -import { relativePath } from 'vs/base/common/resources'; +import { relativePath, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -export interface ILeafNode { - readonly path: string; +export interface ILeafNode { + readonly uri: URI; + readonly relativePath: string; readonly name: string; readonly element: T; + readonly context: C; } -export interface IBranchNode { - readonly path: string; +export interface IBranchNode { + readonly uri: URI; + readonly relativePath: string; readonly name: string; readonly size: number; - readonly children: Iterator>; - readonly parent: IBranchNode | undefined; - get(childName: string): INode | undefined; + readonly children: Iterator>; + readonly parent: IBranchNode | undefined; + readonly context: C; + get(childName: string): INode | undefined; } -export type INode = IBranchNode | ILeafNode; +export type INode = IBranchNode | ILeafNode; // Internals -class Node { +class Node { @memoize - get name(): string { return paths.posix.basename(this.path); } + get name(): string { return paths.posix.basename(this.relativePath); } - constructor(readonly path: string) { } + constructor(readonly uri: URI, readonly relativePath: string, readonly context: C) { } } -class BranchNode extends Node implements IBranchNode { +class BranchNode extends Node implements IBranchNode { - private _children = new Map | LeafNode>(); + private _children = new Map | LeafNode>(); get size(): number { return this._children.size; } - get children(): Iterator | LeafNode> { + get children(): Iterator | LeafNode> { return Iterator.fromIterableIterator(this._children.values()); } - constructor(path: string, readonly parent: IBranchNode | undefined = undefined) { - super(path); + constructor(uri: URI, relativePath: string, context: C, readonly parent: IBranchNode | undefined = undefined) { + super(uri, relativePath, context); } - get(path: string): BranchNode | LeafNode | undefined { + get(path: string): BranchNode | LeafNode | undefined { return this._children.get(path); } - set(path: string, child: BranchNode | LeafNode): void { + set(path: string, child: BranchNode | LeafNode): void { this._children.set(path, child); } @@ -65,22 +69,32 @@ class BranchNode extends Node implements IBranchNode { } } -class LeafNode extends Node implements ILeafNode { +class LeafNode extends Node implements ILeafNode { - constructor(path: string, readonly element: T) { - super(path); + constructor(uri: URI, path: string, context: C, readonly element: T) { + super(uri, path, context); } } -export class ResourceTree> { +function collect(node: INode, result: T[]): T[] { + if (ResourceTree.isBranchNode(node)) { + Iterator.forEach(node.children, child => collect(child, result)); + } else { + result.push(node.element); + } - readonly root = new BranchNode(''); + return result; +} - static isBranchNode(obj: any): obj is IBranchNode { +export class ResourceTree, C> { + + readonly root: BranchNode; + + static isBranchNode(obj: any): obj is IBranchNode { return obj instanceof BranchNode; } - static getRoot(node: IBranchNode): IBranchNode { + static getRoot(node: IBranchNode): IBranchNode { while (node.parent) { node = node.parent; } @@ -88,13 +102,19 @@ export class ResourceTree> { return node; } - constructor(private rootURI: URI = URI.file('/')) { } + static collect(node: INode): T[] { + return collect(node, []); + } + + constructor(context: C, rootURI: URI = URI.file('/')) { + this.root = new BranchNode(rootURI, '', context); + } add(uri: URI, element: T): void { - const key = relativePath(this.rootURI, uri) || uri.fsPath; + const key = relativePath(this.root.uri, uri) || uri.fsPath; const parts = key.split(/[\\\/]/).filter(p => !!p); let node = this.root; - let path = this.root.path; + let path = ''; for (let i = 0; i < parts.length; i++) { const name = parts[i]; @@ -104,10 +124,10 @@ export class ResourceTree> { if (!child) { if (i < parts.length - 1) { - child = new BranchNode(path, node); + child = new BranchNode(joinPath(this.root.uri, path), path, this.root.context, node); node.set(name, child); } else { - child = new LeafNode(path, element); + child = new LeafNode(uri, path, this.root.context, element); node.set(name, child); return; } @@ -119,7 +139,7 @@ export class ResourceTree> { } // replace - node.set(name, new LeafNode(path, element)); + node.set(name, new LeafNode(uri, path, this.root.context, element)); return; } else if (i === parts.length - 1) { throw new Error('Inconsistent tree: can\'t override branch with leaf.'); @@ -130,12 +150,12 @@ export class ResourceTree> { } delete(uri: URI): T | undefined { - const key = relativePath(this.rootURI, uri) || uri.fsPath; + const key = relativePath(this.root.uri, uri) || uri.fsPath; const parts = key.split(/[\\\/]/).filter(p => !!p); return this._delete(this.root, parts, 0); } - private _delete(node: BranchNode, parts: string[], index: number): T | undefined { + private _delete(node: BranchNode, parts: string[], index: number): T | undefined { const name = parts[index]; const child = node.get(name); diff --git a/src/vs/base/test/common/resourceTree.test.ts b/src/vs/base/test/common/resourceTree.test.ts index 6bc77542499..d3050bcd990 100644 --- a/src/vs/base/test/common/resourceTree.test.ts +++ b/src/vs/base/test/common/resourceTree.test.ts @@ -9,24 +9,24 @@ import { URI } from 'vs/base/common/uri'; suite('ResourceTree', function () { test('ctor', function () { - const tree = new ResourceTree(); + const tree = new ResourceTree(null); assert(ResourceTree.isBranchNode(tree.root)); assert.equal(tree.root.size, 0); }); test('simple', function () { - const tree = new ResourceTree(); + const tree = new ResourceTree(null); tree.add(URI.file('/foo/bar.txt'), 'bar contents'); assert(ResourceTree.isBranchNode(tree.root)); assert.equal(tree.root.size, 1); - let foo = tree.root.get('foo') as IBranchNode; + let foo = tree.root.get('foo') as IBranchNode; assert(foo); assert(ResourceTree.isBranchNode(foo)); assert.equal(foo.size, 1); - let bar = foo.get('bar.txt') as ILeafNode; + let bar = foo.get('bar.txt') as ILeafNode; assert(bar); assert(!ResourceTree.isBranchNode(bar)); assert.equal(bar.element, 'bar contents'); @@ -34,14 +34,14 @@ suite('ResourceTree', function () { tree.add(URI.file('/hello.txt'), 'hello contents'); assert.equal(tree.root.size, 2); - let hello = tree.root.get('hello.txt') as ILeafNode; + let hello = tree.root.get('hello.txt') as ILeafNode; assert(hello); assert(!ResourceTree.isBranchNode(hello)); assert.equal(hello.element, 'hello contents'); tree.delete(URI.file('/foo/bar.txt')); assert.equal(tree.root.size, 1); - hello = tree.root.get('hello.txt') as ILeafNode; + hello = tree.root.get('hello.txt') as ILeafNode; assert(hello); assert(!ResourceTree.isBranchNode(hello)); assert.equal(hello.element, 'hello contents'); diff --git a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css index dd900f7dd82..f6937fd9e01 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css +++ b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css @@ -91,6 +91,7 @@ .scm-viewlet .monaco-list-row .resource-group { display: flex; height: 100%; + align-items: center; } .scm-viewlet .monaco-list-row .resource-group > .name { @@ -99,6 +100,7 @@ font-weight: bold; overflow: hidden; text-overflow: ellipsis; + text-decoration: underline; } .scm-viewlet .monaco-list-row .resource { @@ -125,6 +127,7 @@ .scm-viewlet .monaco-list-row .resource-group > .count { padding: 0 8px; + display: flex; } .scm-viewlet .monaco-list-row .resource > .decoration-icon { diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index 6596fb964f8..1d3d1212c13 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -48,14 +48,14 @@ import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { IViewDescriptor } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; -type TreeElement = ISCMResourceGroup | IBranchNode | ISCMResource; +type TreeElement = ISCMResourceGroup | IBranchNode | ISCMResource; interface ResourceGroupTemplate { - name: HTMLElement; - count: CountBadge; - actionBar: ActionBar; + readonly name: HTMLElement; + readonly count: CountBadge; + readonly actionBar: ActionBar; elementDisposables: IDisposable; - disposables: IDisposable; + readonly disposables: IDisposable; } class ResourceGroupRenderer implements ICompressibleTreeRenderer { @@ -130,23 +130,36 @@ class MultipleSelectionActionRunner extends ActionRunner { super(); } - runAction(action: IAction, context: ISCMResource): Promise { - if (action instanceof MenuItemAction) { - const selection = this.getSelectedResources(); - const filteredSelection = selection.filter(s => s !== context); - - if (selection.length === filteredSelection.length || selection.length === 1) { - return action.run(context); - } - - return action.run(context, ...filteredSelection); + runAction(action: IAction, context: ISCMResource | IBranchNode): Promise { + if (!(action instanceof MenuItemAction)) { + return super.runAction(action, context); } - return super.runAction(action, context); + // TODO + // const resources = ResourceTree.isBranchNode(context) + // ? ResourceTree.collect(context) + // : [context]; + + const selection = this.getSelectedResources(); + + if (ResourceTree.isBranchNode(context)) { + const selection = this.getSelectedResources(); + const resources = ResourceTree.collect(context); + + return action.run(...resources, ...selection); + } + + const filteredSelection = selection.filter(s => s !== context); + + if (selection.length === filteredSelection.length || selection.length === 1) { + return action.run(context); + } + + return action.run(context, ...filteredSelection); } } -class ResourceRenderer implements ICompressibleTreeRenderer, FuzzyScore, ResourceTemplate> { +class ResourceRenderer implements ICompressibleTreeRenderer, FuzzyScore, ResourceTemplate> { static TEMPLATE_ID = 'resource'; get templateId(): string { return ResourceRenderer.TEMPLATE_ID; } @@ -176,16 +189,16 @@ class ResourceRenderer implements ICompressibleTreeRenderer | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { + renderElement(node: ITreeNode | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { template.elementDisposables.dispose(); const elementDisposables = new DisposableStore(); - const resource = node.element; + const resourceOrFolder = node.element; const theme = this.themeService.getTheme(); - const icon = ResourceTree.isBranchNode(resource) ? undefined : (theme.type === LIGHT ? resource.decorations.icon : resource.decorations.iconDark); + const icon = !ResourceTree.isBranchNode(resourceOrFolder) && (theme.type === LIGHT ? resourceOrFolder.decorations.icon : resourceOrFolder.decorations.iconDark); - const uri = ResourceTree.isBranchNode(resource) ? URI.file(resource.path) : resource.sourceUri; - const fileKind = ResourceTree.isBranchNode(resource) ? FileKind.FOLDER : FileKind.FILE; + const uri = ResourceTree.isBranchNode(resourceOrFolder) ? resourceOrFolder.uri : resourceOrFolder.sourceUri; + const fileKind = ResourceTree.isBranchNode(resourceOrFolder) ? FileKind.FOLDER : FileKind.FILE; const viewModel = this.viewModelProvider(); template.fileLabel.setFile(uri, { @@ -196,21 +209,19 @@ class ResourceRenderer implements ICompressibleTreeRenderer | ICompressedTreeNode>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { + disposeElement(resource: ITreeNode | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { + template.elementDisposables.dispose(); + } + + renderCompressedElements(node: ITreeNode | ICompressedTreeNode>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { template.elementDisposables.dispose(); - const disposables = new DisposableStore(); - const compressed = node.element as ICompressedTreeNode>; - const branchNode = compressed.elements[compressed.elements.length - 1]; + const elementDisposables = new DisposableStore(); + const compressed = node.element as ICompressedTreeNode>; + const folder = compressed.elements[compressed.elements.length - 1]; const label = compressed.elements.map(e => e.name).join('/'); - const uri = URI.file(branchNode.path); const fileKind = FileKind.FOLDER; - template.fileLabel.setResource({ resource: uri, name: label }, { + template.fileLabel.setResource({ resource: folder.uri, name: label }, { fileDecorations: { colors: false, badges: true }, fileKind, matches: createMatches(node.filterData) }); template.actionBar.clear(); - template.actionBar.context = 'what'; // TODO + template.actionBar.context = folder; - const viewModel = this.viewModelProvider(); - const group = viewModel.getResourceGroupOf(branchNode); - - if (group) { - disposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceFolderMenu(group), template.actionBar)); - } + elementDisposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceFolderMenu(folder.context), template.actionBar)); + removeClass(template.name, 'strike-through'); + removeClass(template.element, 'faded'); template.decorationIcon.style.display = 'none'; template.decorationIcon.style.backgroundImage = ''; - template.element.setAttribute('data-tooltip', branchNode.path); - template.elementDisposables = disposables; + template.element.setAttribute('data-tooltip', ''); + template.elementDisposables = elementDisposables; } - disposeElement(resource: ITreeNode | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { + disposeCompressedElements(node: ITreeNode | ICompressedTreeNode>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { template.elementDisposables.dispose(); } @@ -331,25 +343,27 @@ export class SCMTreeKeyboardNavigationLabelProvider implements IKeyboardNavigati } } -const scmResourceIdentityProvider = new class implements IIdentityProvider { - getId(e: TreeElement): string { - if (ResourceTree.isBranchNode(e)) { - return e.path; - } else if (isSCMResource(e)) { - const group = e.resourceGroup; +class SCMResourceIdentityProvider implements IIdentityProvider { + + getId(element: TreeElement): string { + if (ResourceTree.isBranchNode(element)) { + const group = element.context; + return `${group.provider.contextValue}/${group.id}/$FOLDER/${element.uri.toString()}`; + } else if (isSCMResource(element)) { + const group = element.resourceGroup; const provider = group.provider; - return `${provider.contextValue}/${group.id}/${e.sourceUri.toString()}`; + return `${provider.contextValue}/${group.id}/${element.sourceUri.toString()}`; } else { - const provider = e.provider; - return `${provider.contextValue}/${e.id}`; + const provider = element.provider; + return `${provider.contextValue}/${element.id}`; } } -}; +} interface IGroupItem { readonly group: ISCMResourceGroup; readonly resources: ISCMResource[]; - readonly tree: ResourceTree; + readonly tree: ResourceTree; readonly disposable: IDisposable; } @@ -361,7 +375,7 @@ function groupItemAsTreeElement(item: IGroupItem, mode: ViewModelMode): ICompres return { element: item.group, children, incompressible: true }; } -function asTreeElement(node: INode, incompressible: boolean): ICompressedTreeElement { +function asTreeElement(node: INode, incompressible: boolean): ICompressedTreeElement { if (ResourceTree.isBranchNode(node)) { return { element: node, @@ -406,7 +420,7 @@ class ViewModel { const itemsToInsert: IGroupItem[] = []; for (const group of toInsert) { - const tree = new ResourceTree(group.provider.rootUri || URI.file('/')); + const tree = new ResourceTree(group, group.provider.rootUri || URI.file('/')); const resources: ISCMResource[] = [...group.elements]; const disposable = combinedDisposable( group.onDidChange(() => this.tree.refilter()), @@ -464,18 +478,6 @@ class ViewModel { } } - getResourceGroupOf(node: IBranchNode): 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(); @@ -656,6 +658,7 @@ export class RepositoryPanel extends ViewletPanel { const filter = new SCMTreeFilter(); const sorter = new SCMTreeSorter(); const keyboardNavigationLabelProvider = new SCMTreeKeyboardNavigationLabelProvider(); + const identityProvider = new SCMResourceIdentityProvider(); this.tree = this.instantiationService.createInstance( WorkbenchCompressibleObjectTree, @@ -664,7 +667,7 @@ export class RepositoryPanel extends ViewletPanel { delegate, renderers, { - identityProvider: scmResourceIdentityProvider, + identityProvider, horizontalScrolling: false, filter, sorter, @@ -786,11 +789,7 @@ export class RepositoryPanel extends ViewletPanel { let actions: IAction[] = []; if (ResourceTree.isBranchNode(element)) { - const group = this.viewModel.getResourceGroupOf(element); - - if (group) { - actions = this.menus.getResourceFolderContextActions(group); - } + actions = this.menus.getResourceFolderContextActions(element.context); } else if (isSCMResource(element)) { actions = this.menus.getResourceContextActions(element); } else { From ed37c661f489349611913651a8f2ead37eac492e Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 18 Sep 2019 16:34:16 +0200 Subject: [PATCH 29/42] scm: properly handle action context --- .../contrib/scm/browser/repositoryPanel.ts | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index 1d3d1212c13..d7f8cb11a18 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -47,6 +47,7 @@ import { compareFileNames } from 'vs/base/common/comparers'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { IViewDescriptor } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; +import { flatten } from 'vs/base/common/arrays'; type TreeElement = ISCMResourceGroup | IBranchNode | ISCMResource; @@ -126,7 +127,7 @@ interface ResourceTemplate { class MultipleSelectionActionRunner extends ActionRunner { - constructor(private getSelectedResources: () => ISCMResource[]) { + constructor(private getSelectedResources: () => (ISCMResource | IBranchNode)[]) { super(); } @@ -135,27 +136,11 @@ class MultipleSelectionActionRunner extends ActionRunner { return super.runAction(action, context); } - // TODO - // const resources = ResourceTree.isBranchNode(context) - // ? ResourceTree.collect(context) - // : [context]; - const selection = this.getSelectedResources(); - - if (ResourceTree.isBranchNode(context)) { - const selection = this.getSelectedResources(); - const resources = ResourceTree.collect(context); - - return action.run(...resources, ...selection); - } - - const filteredSelection = selection.filter(s => s !== context); - - if (selection.length === filteredSelection.length || selection.length === 1) { - return action.run(context); - } - - return action.run(context, ...filteredSelection); + const contextIsSelected = selection.some(s => s === context); + const actualContext = contextIsSelected ? selection : [context]; + const args = flatten(actualContext.map(e => ResourceTree.isBranchNode(e) ? ResourceTree.collect(e) : [e])); + return action.run(...args); } } @@ -168,7 +153,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer ViewModel, private labels: ResourceLabels, private actionViewItemProvider: IActionViewItemProvider, - private getSelectedResources: () => ISCMResource[], + private getSelectedResources: () => (ISCMResource | IBranchNode)[], private themeService: IThemeService, private menus: SCMMenus ) { } @@ -804,9 +789,9 @@ export class RepositoryPanel extends ViewletPanel { }); } - private getSelectedResources(): ISCMResource[] { + private getSelectedResources(): (ISCMResource | IBranchNode)[] { return this.tree.getSelection() - .filter(r => !!r && !ResourceTree.isBranchNode(r) && isSCMResource(r)) as ISCMResource[]; + .filter(r => !!r && !isSCMResourceGroup(r))! as any; } private updateInputBox(): void { From b5b28c3c88b0661440d9a06cffbfbc08e940d8a8 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 18 Sep 2019 16:59:05 +0200 Subject: [PATCH 30/42] remove underline --- src/vs/workbench/contrib/scm/browser/media/scmViewlet.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css index f6937fd9e01..54e5506ae01 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css +++ b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css @@ -100,7 +100,6 @@ font-weight: bold; overflow: hidden; text-overflow: ellipsis; - text-decoration: underline; } .scm-viewlet .monaco-list-row .resource { From 911a161f144ab5d100fa7700ba84cf50bf864ca7 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 18 Sep 2019 17:06:57 +0200 Subject: [PATCH 31/42] fix list sorting --- .../workbench/contrib/scm/browser/repositoryPanel.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index d7f8cb11a18..6e6d9730ff9 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -48,6 +48,7 @@ import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { IViewDescriptor } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; import { flatten } from 'vs/base/common/arrays'; +import { memoize } from 'vs/base/common/decorators'; type TreeElement = ISCMResourceGroup | IBranchNode | ISCMResource; @@ -294,7 +295,16 @@ class SCMTreeFilter implements ITreeFilter { export class SCMTreeSorter implements ITreeSorter { + @memoize + private get viewModel(): ViewModel { return this.viewModelProvider(); } + + constructor(private viewModelProvider: () => ViewModel) { } + compare(one: TreeElement, other: TreeElement): number { + if (this.viewModel.mode === ViewModelMode.List) { + return 0; + } + if (isSCMResourceGroup(one) && isSCMResourceGroup(other)) { return 0; } @@ -641,7 +651,7 @@ export class RepositoryPanel extends ViewletPanel { ]; const filter = new SCMTreeFilter(); - const sorter = new SCMTreeSorter(); + const sorter = new SCMTreeSorter(() => this.viewModel); const keyboardNavigationLabelProvider = new SCMTreeKeyboardNavigationLabelProvider(); const identityProvider = new SCMResourceIdentityProvider(); From 75eece48af81f3c75ae58f84e94edd94c6a7ee4b Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 18 Sep 2019 17:10:18 +0200 Subject: [PATCH 32/42] object tree model: use mergeSort --- src/vs/base/browser/ui/tree/objectTreeModel.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/tree/objectTreeModel.ts b/src/vs/base/browser/ui/tree/objectTreeModel.ts index 33fde84435c..d21b8ba3836 100644 --- a/src/vs/base/browser/ui/tree/objectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/objectTreeModel.ts @@ -9,6 +9,7 @@ import { IndexTreeModel, IIndexTreeModelOptions } from 'vs/base/browser/ui/tree/ import { Event } from 'vs/base/common/event'; import { ITreeModel, ITreeNode, ITreeElement, ITreeSorter, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeError } from 'vs/base/browser/ui/tree/tree'; import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; +import { mergeSort } from 'vs/base/common/arrays'; export type ITreeNodeCallback = (node: ITreeNode) => void; @@ -123,7 +124,7 @@ export class ObjectTreeModel, TFilterData extends Non let iterator = elements ? getSequenceIterator(elements) : Iterator.empty>(); if (this.sorter) { - iterator = Iterator.fromArray(Iterator.collect(iterator).sort(this.sorter.compare.bind(this.sorter))); + iterator = Iterator.fromArray(mergeSort(Iterator.collect(iterator), this.sorter.compare.bind(this.sorter))); } return Iterator.map(iterator, treeElement => { From c72500318fb313a1733a3c372c4f3aea9f7ff8fc Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 18 Sep 2019 17:14:03 +0200 Subject: [PATCH 33/42] scm: fix tree sort order --- src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts index 5551203b350..7abfbd15b6a 100644 --- a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts @@ -336,7 +336,7 @@ function mapOptions(compressedNodeUnwrapper: CompressedNodeUnwra ...options, sorter: options.sorter && { compare(node: ICompressedTreeNode, otherNode: ICompressedTreeNode): number { - return options.sorter!.compare(compressedNodeUnwrapper(node), compressedNodeUnwrapper(otherNode)); + return options.sorter!.compare(node.elements[0], otherNode.elements[0]); } }, identityProvider: options.identityProvider && { From 425beff31335b955ab9efccbddd3bd49e3d57fe4 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 18 Sep 2019 17:28:37 +0200 Subject: [PATCH 34/42] scm: fix count --- src/vs/workbench/contrib/scm/browser/repositoryPanel.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index 6e6d9730ff9..5e83a342ee1 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -92,14 +92,11 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer template.count.setCount(group.elements.length); - disposables.add(group.onDidSplice(updateCount, null)); - updateCount(); - template.elementDisposables = disposables; } @@ -112,6 +109,7 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer Date: Wed, 18 Sep 2019 17:39:53 +0200 Subject: [PATCH 35/42] wip: bug --- src/vs/workbench/contrib/scm/browser/repositoryPanel.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index 5e83a342ee1..6d2ad5e8164 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -667,6 +667,9 @@ export class RepositoryPanel extends ViewletPanel { keyboardNavigationLabelProvider }); + // TODO@joao: this breaks the tree + // setTimeout(() => this.tree.refilter(), 2000); + this._register(Event.chain(this.tree.onDidOpen) .map(e => e.elements[0]) .filter(e => !!e && !ResourceTree.isBranchNode(e) && isSCMResource(e)) From aa75677a69cfdc29e18b2edbd44ca548ea82ed9a Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 20 Sep 2019 11:06:50 +0200 Subject: [PATCH 36/42] tree: improve compressible keyboard label navigation provider --- src/vs/base/browser/ui/list/list.ts | 5 ++- src/vs/base/browser/ui/list/listWidget.ts | 31 +++++++------ src/vs/base/browser/ui/tree/abstractTree.ts | 14 +++--- src/vs/base/browser/ui/tree/asyncDataTree.ts | 24 +++++++++-- src/vs/base/browser/ui/tree/objectTree.ts | 43 ++++++++++++++++--- src/vs/platform/list/browser/listService.ts | 17 +++----- .../contrib/scm/browser/repositoryPanel.ts | 19 ++++---- 7 files changed, 104 insertions(+), 49 deletions(-) diff --git a/src/vs/base/browser/ui/list/list.ts b/src/vs/base/browser/ui/list/list.ts index ad6f5eee08c..bb60cd87193 100644 --- a/src/vs/base/browser/ui/list/list.ts +++ b/src/vs/base/browser/ui/list/list.ts @@ -79,7 +79,10 @@ export interface IKeyboardNavigationLabelProvider { * element always match. */ getKeyboardNavigationLabel(element: T): { toString(): string | undefined; } | undefined; - mightProducePrintableCharacter?(event: IKeyboardEvent): boolean; +} + +export interface IKeyboardNavigationDelegate { + mightProducePrintableCharacter(event: IKeyboardEvent): boolean; } export const enum ListDragOverEffect { diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index fa3039d1078..a969eab5f45 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -16,7 +16,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Event, Emitter, EventBufferer } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; -import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction, ListAriaRootRole, ListError } from './list'; +import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction, ListAriaRootRole, ListError, IKeyboardNavigationDelegate } from './list'; import { ListView, IListViewOptions, IListViewDragAndDrop, IAriaProvider } from './listView'; import { Color } from 'vs/base/common/color'; import { mixin } from 'vs/base/common/objects'; @@ -322,16 +322,18 @@ enum TypeLabelControllerState { Typing } -export function mightProducePrintableCharacter(event: IKeyboardEvent): boolean { - if (event.ctrlKey || event.metaKey || event.altKey) { - return false; - } +export const DefaultKeyboardNavigationDelegate = new class implements IKeyboardNavigationDelegate { + mightProducePrintableCharacter(event: IKeyboardEvent): boolean { + if (event.ctrlKey || event.metaKey || event.altKey) { + return false; + } - return (event.keyCode >= KeyCode.KEY_A && event.keyCode <= KeyCode.KEY_Z) - || (event.keyCode >= KeyCode.KEY_0 && event.keyCode <= KeyCode.KEY_9) - || (event.keyCode >= KeyCode.NUMPAD_0 && event.keyCode <= KeyCode.NUMPAD_9) - || (event.keyCode >= KeyCode.US_SEMICOLON && event.keyCode <= KeyCode.US_QUOTE); -} + return (event.keyCode >= KeyCode.KEY_A && event.keyCode <= KeyCode.KEY_Z) + || (event.keyCode >= KeyCode.KEY_0 && event.keyCode <= KeyCode.KEY_9) + || (event.keyCode >= KeyCode.NUMPAD_0 && event.keyCode <= KeyCode.NUMPAD_9) + || (event.keyCode >= KeyCode.US_SEMICOLON && event.keyCode <= KeyCode.US_QUOTE); + } +}; class TypeLabelController implements IDisposable { @@ -347,7 +349,8 @@ class TypeLabelController implements IDisposable { constructor( private list: List, private view: ListView, - private keyboardNavigationLabelProvider: IKeyboardNavigationLabelProvider + private keyboardNavigationLabelProvider: IKeyboardNavigationLabelProvider, + private delegate: IKeyboardNavigationDelegate ) { this.updateOptions(list.options); } @@ -379,7 +382,7 @@ class TypeLabelController implements IDisposable { .filter(e => !isInputElement(e.target as HTMLElement)) .filter(() => this.automaticKeyboardNavigation || this.triggered) .map(event => new StandardKeyboardEvent(event)) - .filter(this.keyboardNavigationLabelProvider.mightProducePrintableCharacter ? e => this.keyboardNavigationLabelProvider.mightProducePrintableCharacter!(e) : e => mightProducePrintableCharacter(e)) + .filter(e => this.delegate.mightProducePrintableCharacter(e)) .forEach(e => { e.stopPropagation(); e.preventDefault(); }) .map(event => event.browserEvent.key) .event; @@ -818,6 +821,7 @@ export interface IListOptions extends IListStyles { readonly enableKeyboardNavigation?: boolean; readonly automaticKeyboardNavigation?: boolean; readonly keyboardNavigationLabelProvider?: IKeyboardNavigationLabelProvider; + readonly keyboardNavigationDelegate?: IKeyboardNavigationDelegate; readonly ariaRole?: ListAriaRootRole; readonly ariaLabel?: string; readonly keyboardSupport?: boolean; @@ -1226,7 +1230,8 @@ export class List implements ISpliceable, IDisposable { } if (_options.keyboardNavigationLabelProvider) { - this.typeLabelController = new TypeLabelController(this, this.view, _options.keyboardNavigationLabelProvider); + const delegate = _options.keyboardNavigationDelegate || DefaultKeyboardNavigationDelegate; + this.typeLabelController = new TypeLabelController(this, this.view, _options.keyboardNavigationLabelProvider, delegate); this.disposables.add(this.typeLabelController); } diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index d390ba15657..e6ab2a8a370 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -5,11 +5,11 @@ import 'vs/css!./media/tree'; import { IDisposable, dispose, Disposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IListOptions, List, IListStyles, mightProducePrintableCharacter, MouseController } from 'vs/base/browser/ui/list/listWidget'; -import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListEvent, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IKeyboardNavigationLabelProvider, IIdentityProvider } from 'vs/base/browser/ui/list/list'; +import { IListOptions, List, IListStyles, MouseController, DefaultKeyboardNavigationDelegate } from 'vs/base/browser/ui/list/listWidget'; +import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListEvent, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IKeyboardNavigationLabelProvider, IIdentityProvider, IKeyboardNavigationDelegate } from 'vs/base/browser/ui/list/list'; import { append, $, toggleClass, getDomNodePagePosition, removeClass, addClass, hasClass, hasParentWithClass, createStyleSheet, clearNode } from 'vs/base/browser/dom'; import { Event, Relay, Emitter, EventBufferer } from 'vs/base/common/event'; -import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator, ICollapseStateChangeEvent, ITreeDragAndDrop, TreeDragOverBubble, TreeVisibility, TreeFilterResult, ITreeModelSpliceEvent, TreeMouseEventTarget } from 'vs/base/browser/ui/tree/tree'; import { ISpliceable } from 'vs/base/common/sequence'; @@ -592,7 +592,7 @@ class TypeFilterController implements IDisposable { model: ITreeModel, private view: List>, private filter: TypeFilter, - private keyboardNavigationLabelProvider: IKeyboardNavigationLabelProvider + private keyboardNavigationDelegate: IKeyboardNavigationDelegate ) { this.domNode = $(`.monaco-list-type-filter.${this.positionClassName}`); this.domNode.draggable = true; @@ -658,13 +658,12 @@ class TypeFilterController implements IDisposable { return; } - const isPrintableCharEvent = this.keyboardNavigationLabelProvider.mightProducePrintableCharacter ? (e: IKeyboardEvent) => this.keyboardNavigationLabelProvider.mightProducePrintableCharacter!(e) : (e: IKeyboardEvent) => mightProducePrintableCharacter(e); const onKeyDown = Event.chain(domEvent(this.view.getHTMLElement(), 'keydown')) .filter(e => !isInputElement(e.target as HTMLElement) || e.target === this.filterOnTypeDomNode) .map(e => new StandardKeyboardEvent(e)) .filter(this.keyboardNavigationEventFilter || (() => true)) .filter(() => this.automaticKeyboardNavigation || this.triggered) - .filter(e => isPrintableCharEvent(e) || ((this.pattern.length > 0 || this.triggered) && ((e.keyCode === KeyCode.Escape || e.keyCode === KeyCode.Backspace) && !e.altKey && !e.ctrlKey && !e.metaKey) || (e.keyCode === KeyCode.Backspace && (isMacintosh ? (e.altKey && !e.metaKey) : e.ctrlKey) && !e.shiftKey))) + .filter(e => this.keyboardNavigationDelegate.mightProducePrintableCharacter(e) || ((this.pattern.length > 0 || this.triggered) && ((e.keyCode === KeyCode.Escape || e.keyCode === KeyCode.Backspace) && !e.altKey && !e.ctrlKey && !e.metaKey) || (e.keyCode === KeyCode.Backspace && (isMacintosh ? (e.altKey && !e.metaKey) : e.ctrlKey) && !e.shiftKey))) .forEach(e => { e.stopPropagation(); e.preventDefault(); }) .event; @@ -1269,7 +1268,8 @@ export abstract class AbstractTree implements IDisposable } if (_options.keyboardNavigationLabelProvider) { - this.typeFilterController = new TypeFilterController(this, this.model, this.view, filter!, _options.keyboardNavigationLabelProvider); + const delegate = _options.keyboardNavigationDelegate || DefaultKeyboardNavigationDelegate; + this.typeFilterController = new TypeFilterController(this, this.model, this.view, filter!, delegate); this.focusNavigationFilter = node => this.typeFilterController!.shouldAllowFocus(node); this.disposables.push(this.typeFilterController!); } diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 1c9aa7ff1e5..6e6efad8cac 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ComposedTreeDelegate, IAbstractTreeOptions, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree'; -import { ObjectTree, IObjectTreeOptions, CompressibleObjectTree, ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; +import { ObjectTree, IObjectTreeOptions, CompressibleObjectTree, ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider, ICompressibleObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree'; import { IListVirtualDelegate, IIdentityProvider, IListDragAndDrop, IListDragOverReaction } from 'vs/base/browser/ui/list/list'; import { ITreeElement, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeSorter, ICollapseStateChangeEvent, IAsyncDataSource, ITreeDragAndDrop, TreeError, WeakMapper } from 'vs/base/browser/ui/tree/tree'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -1010,6 +1010,24 @@ export interface ITreeCompressionDelegate { isIncompressible(element: T): boolean; } +function asCompressibleObjectTreeOptions(options?: ICompressibleAsyncDataTreeOptions): ICompressibleObjectTreeOptions, TFilterData> | undefined { + const objectTreeOptions = options && asObjectTreeOptions(options); + + return objectTreeOptions && { + ...objectTreeOptions, + keyboardNavigationLabelProvider: objectTreeOptions.keyboardNavigationLabelProvider && { + ...objectTreeOptions.keyboardNavigationLabelProvider, + getCompressedNodeKeyboardNavigationLabel(els) { + return options!.keyboardNavigationLabelProvider!.getCompressedNodeKeyboardNavigationLabel(els.map(e => e.element as T)); + } + } + }; +} + +export interface ICompressibleAsyncDataTreeOptions extends IAsyncDataTreeOptions { + readonly keyboardNavigationLabelProvider?: ICompressibleKeyboardNavigationLabelProvider; +} + export class CompressibleAsyncDataTree extends AsyncDataTree { protected readonly compressibleNodeMapper: CompressibleAsyncDataTreeNodeMapper = new WeakMapper(node => new CompressibleAsyncDataTreeNodeWrapper(node)); @@ -1031,11 +1049,11 @@ export class CompressibleAsyncDataTree extends As container: HTMLElement, delegate: IListVirtualDelegate, renderers: ICompressibleTreeRenderer[], - options: IAsyncDataTreeOptions + options: ICompressibleAsyncDataTreeOptions ): ObjectTree, TFilterData> { const objectTreeDelegate = new ComposedTreeDelegate>(delegate); const objectTreeRenderers = renderers.map(r => new CompressibleAsyncDataTreeRenderer(r, this.nodeMapper, () => this.compressibleNodeMapper, this._onDidChangeNodeSlowState.event)); - const objectTreeOptions = asObjectTreeOptions(options) || {}; + const objectTreeOptions = asCompressibleObjectTreeOptions(options) || {}; return new CompressibleObjectTree(user, container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions); } diff --git a/src/vs/base/browser/ui/tree/objectTree.ts b/src/vs/base/browser/ui/tree/objectTree.ts index 243bb7aab21..82f6d1c233c 100644 --- a/src/vs/base/browser/ui/tree/objectTree.ts +++ b/src/vs/base/browser/ui/tree/objectTree.ts @@ -8,9 +8,10 @@ import { AbstractTree, IAbstractTreeOptions } from 'vs/base/browser/ui/tree/abst import { ISpliceable } from 'vs/base/common/sequence'; import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer, ITreeSorter, ICollapseStateChangeEvent } from 'vs/base/browser/ui/tree/tree'; import { ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; -import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IListVirtualDelegate, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list'; import { Event } from 'vs/base/common/event'; import { CompressibleObjectTreeModel, ElementMapper, ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { memoize } from 'vs/base/common/decorators'; export interface IObjectTreeOptions extends IAbstractTreeOptions { sorter?: ITreeSorter; @@ -77,9 +78,12 @@ class CompressibleRenderer implements ITreeRender readonly templateId: string; readonly onDidChangeTwistieState: Event | undefined; - compressedTreeNodeProvider: ICompressedTreeNodeProvider; + @memoize + private get compressedTreeNodeProvider(): ICompressedTreeNodeProvider { + return this._compressedTreeNodeProvider(); + } - constructor(private renderer: ICompressibleTreeRenderer) { + constructor(private _compressedTreeNodeProvider: () => ICompressedTreeNodeProvider, private renderer: ICompressibleTreeRenderer) { this.templateId = renderer.templateId; if (renderer.onDidChangeTwistieState) { @@ -127,6 +131,31 @@ class CompressibleRenderer implements ITreeRender } } +export interface ICompressibleKeyboardNavigationLabelProvider extends IKeyboardNavigationLabelProvider { + getCompressedNodeKeyboardNavigationLabel(elements: T[]): { toString(): string | undefined; } | undefined; +} + +export interface ICompressibleObjectTreeOptions extends IObjectTreeOptions { + readonly keyboardNavigationLabelProvider?: ICompressibleKeyboardNavigationLabelProvider; +} + +function asObjectTreeOptions(compressedTreeNodeProvider: () => ICompressedTreeNodeProvider, options?: ICompressibleObjectTreeOptions): IObjectTreeOptions | undefined { + return options && { + ...options, + keyboardNavigationLabelProvider: options.keyboardNavigationLabelProvider && { + getKeyboardNavigationLabel(e: T) { + const compressedTreeNode = compressedTreeNodeProvider().getCompressedTreeNode(e); + + if (compressedTreeNode.element.elements.length === 1) { + return options.keyboardNavigationLabelProvider!.getKeyboardNavigationLabel(e); + } else { + return options.keyboardNavigationLabelProvider!.getCompressedNodeKeyboardNavigationLabel(compressedTreeNode.element.elements); + } + } + } + }; +} + export class CompressibleObjectTree, TFilterData = void> extends ObjectTree { protected model: CompressibleObjectTreeModel; @@ -136,11 +165,11 @@ export class CompressibleObjectTree, TFilterData = vo container: HTMLElement, delegate: IListVirtualDelegate, renderers: ICompressibleTreeRenderer[], - options: IObjectTreeOptions = {} + options: ICompressibleObjectTreeOptions = {} ) { - const compressibleRenderers = renderers.map(r => new CompressibleRenderer(r)); - super(user, container, delegate, compressibleRenderers, options); - compressibleRenderers.forEach(r => r.compressedTreeNodeProvider = this); + const compressedTreeNodeProvider = () => this; + const compressibleRenderers = renderers.map(r => new CompressibleRenderer(compressedTreeNodeProvider, r)); + super(user, container, delegate, compressibleRenderers, asObjectTreeOptions(compressedTreeNodeProvider, options)); } setChildren(element: T | null, children?: ISequence>): void { diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 65037724705..e3c42e0b054 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -25,7 +25,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { attachListStyler, computeStyles, defaultListStyles } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; -import { ObjectTree, IObjectTreeOptions, ICompressibleTreeRenderer, CompressibleObjectTree } from 'vs/base/browser/ui/tree/objectTree'; +import { ObjectTree, IObjectTreeOptions, ICompressibleTreeRenderer, CompressibleObjectTree, ICompressibleObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree'; import { ITreeEvent, ITreeRenderer, IAsyncDataSource, IDataSource, ITreeMouseEvent } from 'vs/base/browser/ui/tree/tree'; import { AsyncDataTree, IAsyncDataTreeOptions, CompressibleAsyncDataTree, ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; import { DataTree, IDataTreeOptions } from 'vs/base/browser/ui/tree/dataTree'; @@ -212,14 +212,11 @@ function toWorkbenchListOptions(options: IListOptions, configurationServic result.openController = openController; disposables.add(openController); - if (options.keyboardNavigationLabelProvider) { - const tlp = options.keyboardNavigationLabelProvider; - - result.keyboardNavigationLabelProvider = { - getKeyboardNavigationLabel(e) { return tlp.getKeyboardNavigationLabel(e); }, - mightProducePrintableCharacter(e) { return keybindingService.mightProducePrintableCharacter(e); } - }; - } + result.keyboardNavigationDelegate = { + mightProducePrintableCharacter(e) { + return keybindingService.mightProducePrintableCharacter(e); + } + }; return [result, disposables]; } @@ -818,7 +815,7 @@ export class WorkbenchCompressibleObjectTree, TFilter container: HTMLElement, delegate: IListVirtualDelegate, renderers: ICompressibleTreeRenderer[], - options: IObjectTreeOptions, + options: ICompressibleObjectTreeOptions, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IThemeService themeService: IThemeService, diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index 6d2ad5e8164..d243f9d22dd 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -10,7 +10,7 @@ import { basename } from 'vs/base/common/resources'; import { IDisposable, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle'; import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; import { append, $, addClass, toggleClass, trackFocus, removeClass } from 'vs/base/browser/dom'; -import { IListVirtualDelegate, IKeyboardNavigationLabelProvider, IIdentityProvider } from 'vs/base/browser/ui/list/list'; +import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { ISCMRepository, ISCMResourceGroup, ISCMResource, InputValidationType } from 'vs/workbench/contrib/scm/common/scm'; import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; @@ -38,7 +38,7 @@ 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 { ResourceTree, IBranchNode, INode } from 'vs/base/common/resourceTree'; -import { ObjectTree, ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; +import { ObjectTree, ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/tree/objectTree'; import { Iterator } from 'vs/base/common/iterator'; import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { URI } from 'vs/base/common/uri'; @@ -321,18 +321,21 @@ export class SCMTreeSorter implements ITreeSorter { } } -export class SCMTreeKeyboardNavigationLabelProvider implements IKeyboardNavigationLabelProvider { +export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyboardNavigationLabelProvider { getKeyboardNavigationLabel(element: TreeElement): { toString(): string; } | undefined { - if (isSCMResourceGroup(element)) { + if (ResourceTree.isBranchNode(element)) { + return element.name; + } else if (isSCMResourceGroup(element)) { return element.label; - } - - if (isSCMResource(element)) { + } else { return basename(element.sourceUri); } + } - return ''; + getCompressedNodeKeyboardNavigationLabel(elements: TreeElement[]): { toString(): string | undefined; } | undefined { + const folders = elements as IBranchNode[]; + return folders.map(e => e.name).join('/'); } } From df6d49c1b1eaa3261912c14bb6d758e2b3fdc7f6 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 20 Sep 2019 11:27:32 +0200 Subject: [PATCH 37/42] index tree: fix splice issue with filtered nodes --- src/vs/base/browser/ui/tree/indexTreeModel.ts | 2 +- .../browser/ui/tree/indexTreeModel.test.ts | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/tree/indexTreeModel.ts b/src/vs/base/browser/ui/tree/indexTreeModel.ts index 6911c5fac8d..182caa98612 100644 --- a/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -169,7 +169,7 @@ export class IndexTreeModel, TFilterData = voi parentNode.visibleChildrenCount += insertedVisibleChildrenCount - deletedVisibleChildrenCount; if (revealed && visible) { - const visibleDeleteCount = deletedNodes.reduce((r, node) => r + node.renderNodeCount, 0); + const visibleDeleteCount = deletedNodes.reduce((r, node) => r + (node.visible ? node.renderNodeCount : 0), 0); this._updateAncestorsRenderNodeCount(parentNode, renderNodeCount - visibleDeleteCount); this.list.splice(listIndex, visibleDeleteCount, treeListElementsToInsert); diff --git a/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts index 01a42eae236..e11b73ed9d8 100644 --- a/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts @@ -690,4 +690,38 @@ suite('IndexTreeModel', function () { assert.deepEqual(model.getNodeLocation(list[3]), [0, 5]); }); }); + + test('refilter with filtered out nodes', function () { + const list: ITreeNode[] = []; + let query = new RegExp(''); + const filter = new class implements ITreeFilter { + filter(element: string): boolean { + return query.test(element); + } + }; + + const model = new IndexTreeModel('test', toSpliceable(list), 'root', { filter }); + + model.splice([0], 0, Iterator.fromArray([ + { element: 'silver' }, + { element: 'gold' }, + { element: 'platinum' } + ])); + + assert.deepEqual(toArray(list), ['silver', 'gold', 'platinum']); + + query = /platinum/; + model.refilter(); + assert.deepEqual(toArray(list), ['platinum']); + + model.splice([0], Number.POSITIVE_INFINITY, Iterator.fromArray([ + { element: 'silver' }, + { element: 'gold' }, + { element: 'platinum' } + ])); + assert.deepEqual(toArray(list), ['platinum']); + + model.refilter(); + assert.deepEqual(toArray(list), ['platinum']); + }); }); From 210de8848868ce9975ef16d5ace335530cb15ea9 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 20 Sep 2019 11:30:23 +0200 Subject: [PATCH 38/42] compressible object tree: fix keyboard navigation for new nodes --- src/vs/base/browser/ui/tree/objectTree.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/tree/objectTree.ts b/src/vs/base/browser/ui/tree/objectTree.ts index 82f6d1c233c..74203443bac 100644 --- a/src/vs/base/browser/ui/tree/objectTree.ts +++ b/src/vs/base/browser/ui/tree/objectTree.ts @@ -144,7 +144,13 @@ function asObjectTreeOptions(compressedTreeNodeProvider: () => I ...options, keyboardNavigationLabelProvider: options.keyboardNavigationLabelProvider && { getKeyboardNavigationLabel(e: T) { - const compressedTreeNode = compressedTreeNodeProvider().getCompressedTreeNode(e); + let compressedTreeNode: ITreeNode, TFilterData>; + + try { + compressedTreeNode = compressedTreeNodeProvider().getCompressedTreeNode(e); + } catch { + return options.keyboardNavigationLabelProvider!.getKeyboardNavigationLabel(e); + } if (compressedTreeNode.element.elements.length === 1) { return options.keyboardNavigationLabelProvider!.getKeyboardNavigationLabel(e); From 7186fef89d07f85b09ea5df7813504caf80cf707 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 20 Sep 2019 11:50:05 +0200 Subject: [PATCH 39/42] cleanup --- src/vs/workbench/contrib/scm/browser/repositoryPanel.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index d243f9d22dd..998f536012c 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -670,9 +670,6 @@ export class RepositoryPanel extends ViewletPanel { keyboardNavigationLabelProvider }); - // TODO@joao: this breaks the tree - // setTimeout(() => this.tree.refilter(), 2000); - this._register(Event.chain(this.tree.onDidOpen) .map(e => e.elements[0]) .filter(e => !!e && !ResourceTree.isBranchNode(e) && isSCMResource(e)) From 3a7ac68889f2266d95c01fc0201c52ab3e26b29f Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 20 Sep 2019 16:13:55 +0200 Subject: [PATCH 40/42] tree: fix npe --- src/vs/base/browser/ui/tree/abstractTree.ts | 22 ++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index e6ab2a8a370..610a5374551 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -1228,7 +1228,7 @@ export abstract class AbstractTree implements IDisposable const treeDelegate = new ComposedTreeDelegate>(delegate); const onDidChangeCollapseStateRelay = new Relay>(); - const onDidChangeActiveNodes = new Relay[]>(); + const onDidChangeActiveNodes = new Emitter[]>(); const activeNodes = new EventCollection(onDidChangeActiveNodes.event); this.disposables.push(activeNodes); @@ -1251,11 +1251,23 @@ export abstract class AbstractTree implements IDisposable onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState; this.model.onDidSplice(e => { - this.focus.onDidModelSplice(e); - this.selection.onDidModelSplice(e); - }, null, this.disposables); + this.eventBufferer.bufferEvents(() => { + this.focus.onDidModelSplice(e); + this.selection.onDidModelSplice(e); + }); - onDidChangeActiveNodes.input = Event.map(Event.any(this.focus.onDidChange, this.selection.onDidChange, this.model.onDidSplice), () => [...this.focus.getNodes(), ...this.selection.getNodes()]); + const set = new Set>(); + + for (const node of this.focus.getNodes()) { + set.add(node); + } + + for (const node of this.selection.getNodes()) { + set.add(node); + } + + onDidChangeActiveNodes.fire(Array.from(set.values())); + }, null, this.disposables); if (_options.keyboardSupport !== false) { const onKeyDown = Event.chain(this.view.onKeyDown) From 04bfb983d7fb0b33f8a9d3acc3164fc12c4ce459 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 20 Sep 2019 16:43:22 +0200 Subject: [PATCH 41/42] scm tree: proper icon theme alignment --- .../browser/parts/views/media/views.css | 6 +++--- .../contrib/scm/browser/media/scmViewlet.css | 6 ++++++ .../contrib/scm/browser/repositoryPanel.ts | 19 ++++++++++++++++++- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 2e5ebe1420b..1127eae5c34 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -52,9 +52,9 @@ /* File icons in trees */ -.file-icon-themable-tree.align-icons-and-twisties .monaco-tl-twistie:not(.collapsible), -.file-icon-themable-tree .align-icon-with-twisty .monaco-tl-twistie:not(.collapsible), -.file-icon-themable-tree.hide-arrows .monaco-tl-twistie { +.file-icon-themable-tree.align-icons-and-twisties .monaco-tl-twistie:not(.force-twistie):not(.collapsible), +.file-icon-themable-tree .align-icon-with-twisty .monaco-tl-twistie:not(.force-twistie):not(.collapsible), +.file-icon-themable-tree.hide-arrows .monaco-tl-twistie:not(.force-twistie) { background-image: none !important; width: 0 !important; margin-right: 0 !important; diff --git a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css index 54e5506ae01..5aab7077548 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css +++ b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css @@ -192,3 +192,9 @@ .scm-viewlet .scm-editor.scroll > .monaco-inputbox > .wrapper > textarea.input { overflow-y: scroll; } + +.scm-viewlet .list-view-mode .monaco-tl-twistie:not(.force-twistie):not(.collapsible) { + background-image: none !important; + width: 8px !important; + margin-right: 0 !important; +} diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index 998f536012c..5919689b242 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -49,6 +49,7 @@ import { IViewDescriptor } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; import { flatten } from 'vs/base/common/arrays'; import { memoize } from 'vs/base/common/decorators'; +import { IWorkbenchThemeService, IFileIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; type TreeElement = ISCMResourceGroup | IBranchNode | ISCMResource; @@ -72,6 +73,9 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer { + toggleClass(this.listContainer, 'list-view-mode', this.viewModel.mode === ViewModelMode.List); + toggleClass(this.listContainer, 'align-icons-and-twisties', this.viewModel.mode === ViewModelMode.Tree && theme.hasFileIcons && !theme.hasFolderIcons); + toggleClass(this.listContainer, 'hide-arrows', this.viewModel.mode === ViewModelMode.Tree && theme.hidesExplorerArrows === true); + }; + + updateIndentStyles(this.themeService.getFileIconTheme()); + this._register(this.themeService.onDidFileIconThemeChange(updateIndentStyles)); + this._register(this.viewModel.onDidChangeMode(() => updateIndentStyles(this.themeService.getFileIconTheme()))); + this.toggleViewModelModeAction = new ToggleViewModeAction(this.viewModel); this._register(this.toggleViewModelModeAction); From ef326b9057e0e15b41a182925b9acd250e702321 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 20 Sep 2019 17:03:43 +0200 Subject: [PATCH 42/42] scm: restore scrollTop --- src/vs/workbench/contrib/scm/browser/repositoryPanel.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts index 5919689b242..a1a17249418 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPanel.ts @@ -393,7 +393,6 @@ const enum ViewModelMode { Tree = 'tree' } -// TODO: cache tree scrollTop class ViewModel { private _mode = ViewModelMode.Tree; @@ -409,6 +408,7 @@ class ViewModel { private items: IGroupItem[] = []; private visibilityDisposables = new DisposableStore(); + private scrollTop: number | undefined; private disposables = new DisposableStore(); constructor( @@ -464,9 +464,15 @@ class ViewModel { this.visibilityDisposables = new DisposableStore(); this.groups.onDidSplice(this.onDidSpliceGroups, this, this.visibilityDisposables); this.onDidSpliceGroups({ start: 0, deleteCount: this.items.length, toInsert: this.groups.elements }); + + if (typeof this.scrollTop === 'number') { + this.tree.scrollTop = this.scrollTop; + this.scrollTop = undefined; + } } else { this.visibilityDisposables.dispose(); this.onDidSpliceGroups({ start: 0, deleteCount: this.items.length, toInsert: [] }); + this.scrollTop = this.tree.scrollTop; } }