From 5aed010f519297ca5aaa4fd5317e745e9219a05a Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 22 Oct 2020 21:33:36 +0200 Subject: [PATCH] SCM viewlet filter on type improvements (#109170) --- src/vs/base/browser/ui/list/list.ts | 8 +- src/vs/base/browser/ui/tree/abstractTree.ts | 36 +++--- .../contrib/scm/browser/scmViewPane.ts | 115 +++++++++++------- 3 files changed, 92 insertions(+), 67 deletions(-) diff --git a/src/vs/base/browser/ui/list/list.ts b/src/vs/base/browser/ui/list/list.ts index 25a09d22fae..4f8e796f206 100644 --- a/src/vs/base/browser/ui/list/list.ts +++ b/src/vs/base/browser/ui/list/list.ts @@ -66,11 +66,11 @@ export interface IIdentityProvider { export interface IKeyboardNavigationLabelProvider { /** - * Return a keyboard navigation label which will be used by the - * list for filtering/navigating. Return `undefined` to make an - * element always match. + * Return a keyboard navigation label(s) which will be used by + * the list for filtering/navigating. Return `undefined` to make + * an element always match. */ - getKeyboardNavigationLabel(element: T): { toString(): string | undefined; } | undefined; + getKeyboardNavigationLabel(element: T): { toString(): string | undefined; } | { toString(): string | undefined; }[] | undefined; } export interface IKeyboardNavigationDelegate { diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index db296ffd300..a188ee41f00 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -507,8 +507,9 @@ class TreeRenderer implements IListRenderer } } -class TypeFilter implements ITreeFilter, IDisposable { +export type LabelFuzzyScore = { label: string; score: FuzzyScore }; +class TypeFilter implements ITreeFilter, IDisposable { private _totalCount = 0; get totalCount(): number { return this._totalCount; } private _matchCount = 0; @@ -531,7 +532,7 @@ class TypeFilter implements ITreeFilter, IDisposable { tree.onWillRefilter(this.reset, this, this.disposables); } - filter(element: T, parentVisibility: TreeVisibility): TreeFilterResult { + filter(element: T, parentVisibility: TreeVisibility): TreeFilterResult { if (this._filter) { const result = this._filter.filter(element, parentVisibility); @@ -562,27 +563,28 @@ class TypeFilter implements ITreeFilter, IDisposable { } const label = this.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(element); - const labelStr = label && label.toString(); + const labels = Array.isArray(label) ? label : [label]; - if (typeof labelStr === 'undefined') { - return { data: FuzzyScore.Default, visibility: true }; - } - - const score = fuzzyScore(this._pattern, this._lowercasePattern, 0, labelStr, labelStr.toLowerCase(), 0, true); - - if (!score) { - if (this.tree.options.filterOnType) { - return TreeVisibility.Recurse; - } else { + for (const l of labels) { + const labelStr = l && l.toString(); + if (typeof labelStr === 'undefined') { return { data: FuzzyScore.Default, visibility: true }; } - // DEMO: smarter filter ? - // return parentVisibility === TreeVisibility.Visible ? true : TreeVisibility.Recurse; + const score = fuzzyScore(this._pattern, this._lowercasePattern, 0, labelStr, labelStr.toLowerCase(), 0, true); + if (score) { + this._matchCount++; + return labels.length === 1 ? + { data: score, visibility: true } : + { data: { label: labelStr, score: score }, visibility: true }; + } } - this._matchCount++; - return { data: score, visibility: true }; + if (this.tree.options.filterOnType) { + return TreeVisibility.Recurse; + } else { + return { data: FuzzyScore.Default, visibility: true }; + } } private reset(): void { diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index c711da66f2e..12d99470918 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -77,42 +77,10 @@ import { RepositoryRenderer } from 'vs/workbench/contrib/scm/browser/scmReposito import { IPosition } from 'vs/editor/common/core/position'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { LabelFuzzyScore } from 'vs/base/browser/ui/tree/abstractTree'; type TreeElement = ISCMRepository | ISCMInput | ISCMResourceGroup | IResourceNode | ISCMResource; -function splitMatches(uri: URI, filterData: FuzzyScore | undefined): [IMatch[] | undefined, IMatch[] | undefined] { - let matches: IMatch[] | undefined; - let descriptionMatches: IMatch[] | undefined; - - if (filterData) { - matches = []; - descriptionMatches = []; - - const fileName = basename(uri); - const allMatches = createMatches(filterData); - - for (const match of allMatches) { - if (match.start < fileName.length) { - matches!.push( - { - start: match.start, - end: Math.min(match.end, fileName.length) - } - ); - } else { - descriptionMatches!.push( - { - start: match.start - (fileName.length + 1), - end: match.end - (fileName.length + 1) - } - ); - } - } - } - - return [matches, descriptionMatches]; -} - interface ISCMLayout { height: number | undefined; width: number | undefined; @@ -343,7 +311,7 @@ class RepositoryPaneActionRunner extends ActionRunner { } } -class ResourceRenderer implements ICompressibleTreeRenderer, FuzzyScore, ResourceTemplate> { +class ResourceRenderer implements ICompressibleTreeRenderer, FuzzyScore | LabelFuzzyScore, ResourceTemplate> { static readonly TEMPLATE_ID = 'resource'; get templateId(): string { return ResourceRenderer.TEMPLATE_ID; } @@ -373,7 +341,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { + renderElement(node: ITreeNode | ITreeNode, FuzzyScore | LabelFuzzyScore>, index: number, template: ResourceTemplate): void { template.elementDisposables.dispose(); const elementDisposables = new DisposableStore(); @@ -397,13 +365,14 @@ class ResourceRenderer implements ICompressibleTreeRenderer | ITreeNode, FuzzyScore>, index: number, template: ResourceTemplate): void { + disposeElement(resource: ITreeNode | ITreeNode, FuzzyScore | LabelFuzzyScore>, index: number, template: ResourceTemplate): void { template.elementDisposables.dispose(); } - renderCompressedElements(node: ITreeNode | ICompressedTreeNode>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { + renderCompressedElements(node: ITreeNode | ICompressedTreeNode>, FuzzyScore | LabelFuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { template.elementDisposables.dispose(); const elementDisposables = new DisposableStore(); @@ -454,9 +423,11 @@ class ResourceRenderer implements ICompressibleTreeRenderer e.name).join('/'); const fileKind = FileKind.FOLDER; + const matches = createMatches(node.filterData as FuzzyScore | undefined); template.fileLabel.setResource({ resource: folder.uri, name: label }, { fileDecorations: { colors: false, badges: true }, - fileKind + fileKind, + matches }); template.actionBar.clear(); @@ -474,7 +445,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer | ICompressedTreeNode>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { + disposeCompressedElements(node: ITreeNode | ICompressedTreeNode>, FuzzyScore | LabelFuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void { template.elementDisposables.dispose(); } @@ -482,6 +453,56 @@ class ResourceRenderer implements ICompressibleTreeRenderer pathLength) { + // Label match + labelMatches.push({ + start: match.start - pathLength, + end: match.end - pathLength + }); + } else if (match.end < pathLength) { + // Description match + descriptionMatches.push(match); + } else { + // Spanning match + labelMatches.push({ + start: 0, + end: match.end - pathLength + }); + descriptionMatches.push({ + start: match.start, + end: pathLength + }); + } + } + + return [labelMatches, descriptionMatches]; + } } class ListDelegate implements IListVirtualDelegate { @@ -601,7 +622,7 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb @ILabelService private readonly labelService: ILabelService, ) { } - getKeyboardNavigationLabel(element: TreeElement): { toString(): string; } | undefined { + getKeyboardNavigationLabel(element: TreeElement): { toString(): string; } | { toString(): string; }[] | undefined { if (ResourceTree.isResourceNode(element)) { return element.name; } else if (isSCMRepository(element)) { @@ -614,11 +635,13 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb const viewModel = this.viewModelProvider(); if (viewModel.mode === ViewModelMode.List) { // In List mode match using the file name and the path. - // Since a match in the file name takes precedence over a match - // in the folder name we are returning the label as file folder. + // Since we want to match both on the file name and the + // full path we return an array of labels. A match in the + // file name takes precedence over a match in the path. const fileName = basename(element.sourceUri); - const filePath = this.labelService.getUriLabel(dirname(element.sourceUri), { relative: true }); - return filePath.length !== 0 ? `${fileName} ${filePath}` : fileName; + const filePath = this.labelService.getUriLabel(element.sourceUri, { relative: true }); + + return [fileName, filePath]; } else { // In Tree mode only match using the file name return basename(element.sourceUri); @@ -1637,7 +1660,7 @@ export class SCMViewPane extends ViewPane { this._register(actionRunner); this._register(actionRunner.onDidBeforeRun(() => this.tree.domFocus())); - const renderers: ICompressibleTreeRenderer[] = [ + const renderers: ICompressibleTreeRenderer[] = [ this.instantiationService.createInstance(RepositoryRenderer, actionViewItemProvider), this.inputRenderer, this.instantiationService.createInstance(ResourceGroupRenderer, actionViewItemProvider), -- GitLab