Change:
I often make a lot of changes at once, and then need to commit them. Some changes don't belong together in the same commit, and some aren't ready to be committed at all. The current UI allows me to select some files to be staged for commit and exclude others, but it requires me to carefully select my changes from a potentially very long list. It would be incredibly useful if I could search or filter files by path pattern and stage/unstage all search results. Even better would be if I could search for lines of code and either stage files containing those lines or just the matched lines. This would streamline my git workflow by a lot.
I guess my ideal UI would allow me to: search files by path, and stage/unstage all matched files stage/unstage individually selected files search code in files, and stage/unstage all matched lines stage/unstage individually selected lines stage/unstage all matched files stage/unstage individually selected files
Since last month, SCM supports fancy keyboard navigation and filtering. Simply start typing when focused on the view. A widget will appear which you can use to toggle between simple highlighting and filtering. Currently filtering is done only on file names though, not paths or even code, so I can leave this open for additional filtering fields.
<!-- -96bd-47a8-a0e8- --This feature request is now a candidate for our backlog. The community has 60 days to upvote the issue. If it receives 20 upvotes we will move it to our backlog. If not, we will close it. To learn more about how we handle feature requests, please see our . Happy Coding!

Discussion:
italic: this.options?.italic, strikethrough: this.options?.strikethrough, matches: this.options?.matches, <ins> descriptionMatches: this.options?.descriptionMatches, </ins> extraClasses: [], separator: this.options?.separator, domId: this.options?.domId

import { URI } from 'vs/base/common/uri'; import { FileKind } from 'vs/platform/files/common/files'; import { compareFileNames } from 'vs/base/common/comparers'; <del> import { FuzzyScore, createMatches } from 'vs/base/common/filters'; </del> <ins> import { FuzzyScore, createMatches, IMatch } from 'vs/base/common/filters'; </ins> import { IViewDescriptor, IViewDescriptorService } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; import { flatten, find } from 'vs/base/common/arrays';

type TreeElement = ISCMResourceGroup | IResourceNode<ISCMResource, ISCMResourceGroup> | ISCMResource; <ins> 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.end <= fileName.length) { matches!.push(match); } else { descriptionMatches!.push( { start: match.start - fileName.length, end: match.end - fileName.length } ); } } } return [matches, descriptionMatches]; } </ins> interface ResourceGroupTemplate { readonly name: HTMLElement; readonly count: CountBadge;

renderTemplate(container: HTMLElement): ResourceTemplate { const element = append(container, $('.resource')); const name = append(element, $('.name')); <del> const fileLabel = this.labels.create(name, { supportHighlights: true }); </del> <ins> const fileLabel = this.labels.create(name, { supportDescriptionHighlights: true, supportHighlights: true }); </ins> const actionsContainer = append(fileLabel.element, $('.actions')); const actionBar = new ActionBar(actionsContainer, { actionViewItemProvider: this.actionViewItemProvider,

const fileKind = ResourceTree.isResourceNode(resourceOrFolder) ? FileKind.FOLDER : FileKind.FILE; const viewModel = this.viewModelProvider(); <ins> const [matches, descriptionMatches] = splitMatches(uri, node.filterData); </ins> template.fileLabel.setFile(uri, { fileDecorations: { colors: false, badges: !icon }, hidePath: viewModel.mode === ViewModelMode.Tree, fileKind, <del> matches: createMatches(node.filterData) </del> <ins> matches, descriptionMatches </ins> }); template.actionBar.clear();

const label = compressed.elements.map(e => e.name).join('/'); const fileKind = FileKind.FOLDER; <ins> const [matches, descriptionMatches] = splitMatches(folder.uri, node.filterData); </ins> template.fileLabel.setResource({ resource: folder.uri, name: label }, { fileDecorations: { colors: false, badges: true }, fileKind, <del> matches: createMatches(node.filterData) </del> <ins> matches, descriptionMatches </ins> }); template.actionBar.clear();

export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyboardNavigationLabelProvider<TreeElement> { <ins> constructor(@ILabelService private readonly labelService: ILabelService) { } </ins> getKeyboardNavigationLabel(element: TreeElement): { toString(): string; } | undefined { if (ResourceTree.isResourceNode(element)) { return element.name; } else if (isSCMResourceGroup(element)) { return element.label; } else { <del> return basename(element.sourceUri); </del> <ins> // Since a match in the file name takes precedence over a match // in the folder name we are returning the label as file/folder. const fileName = basename(element.sourceUri); const filePath = this.labelService.getUriLabel(dirname(element.sourceUri), { relative: true }); return filePath.length !== 0 ? `${fileName}${filePath}` : fileName; </ins> } }

const filter = new SCMTreeFilter(); const sorter = new SCMTreeSorter(() => this.viewModel); <del> const keyboardNavigationLabelProvider = new SCMTreeKeyboardNavigationLabelProvider(); </del> <ins> const keyboardNavigationLabelProvider = this.instantiationService.createInstance(SCMTreeKeyboardNavigationLabelProvider); </ins> const identityProvider = new SCMResourceIdentityProvider(); this.tree = this.instantiationService.createInstance(