提交 9da0fa19 编写于 作者: R Rob Lourens 提交者: GitHub

Merge pull request #29964 from keegancsmith/foldermatch

Group search results by root folders
......@@ -12,7 +12,7 @@ import { IExtensionService } from 'vs/platform/extensions/common/extensions';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IEditorInput } from 'vs/platform/editor/common/editor';
import { toResource } from 'vs/workbench/common/editor';
import { getPathLabel } from 'vs/base/common/labels';
import { getPathLabel, IRootProvider } from 'vs/base/common/labels';
import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
......@@ -168,6 +168,7 @@ export class EditorLabel extends ResourceLabel {
export interface IFileLabelOptions extends IResourceLabelOptions {
hideLabel?: boolean;
hidePath?: boolean;
root?: uri;
}
export class FileLabel extends ResourceLabel {
......@@ -188,11 +189,14 @@ export class FileLabel extends ResourceLabel {
public setFile(resource: uri, options: IFileLabelOptions = Object.create(null)): void {
const hidePath = options.hidePath || (resource.scheme === Schemas.untitled && !this.untitledEditorService.hasAssociatedFilePath(resource));
const rootProvider: IRootProvider = options.root ? {
getRoot(): uri { return options.root; },
getWorkspace(): { roots: uri[]; } { return { roots: [options.root] }; },
} : this.contextService;
this.setLabel({
resource,
name: !options.hideLabel ? paths.basename(resource.fsPath) : void 0,
description: !hidePath ? getPathLabel(paths.dirname(resource.fsPath), this.contextService, this.environmentService) : void 0
description: !hidePath ? getPathLabel(paths.dirname(resource.fsPath), rootProvider, this.environmentService) : void 0
}, options);
}
}
......
......@@ -170,6 +170,7 @@
overflow: hidden;
}
.search-viewlet .foldermatch,
.search-viewlet .filematch {
display: flex;
position: relative;
......@@ -177,10 +178,12 @@
padding: 0;
}
.search-viewlet .foldermatch .monaco-icon-label,
.search-viewlet .filematch .monaco-icon-label {
flex: 1;
}
.search-viewlet .foldermatch .directory,
.search-viewlet .filematch .directory {
opacity: 0.7;
font-size: 0.9em;
......@@ -266,9 +269,11 @@
margin-right: 12px;
}
.search-viewlet > .results > .monaco-tree .monaco-tree-row:hover .content .monaco-count-badge,
.search-viewlet > .results > .monaco-tree.focused .monaco-tree-row.focused .content .monaco-count-badge {
display: none;
.search-viewlet > .results > .monaco-tree .monaco-tree-row:hover .content .filematch .monaco-count-badge,
.search-viewlet > .results > .monaco-tree .monaco-tree-row:hover .content .linematch .monaco-count-badge,
.search-viewlet > .results > .monaco-tree.focused .monaco-tree-row.focused .content .filematch .monaco-count-badge,
.search-viewlet > .results > .monaco-tree.focused .monaco-tree-row.focused .content .linematch .monaco-count-badge {
display: none;
}
.search-viewlet .focused .monaco-tree-row.selected:not(.highlighted) > .content.actions .action-remove,
......@@ -382,6 +387,7 @@
opacity: .5;
}
.vs-dark .search-viewlet .foldermatch,
.vs-dark .search-viewlet .filematch {
padding: 0;
}
......@@ -398,6 +404,7 @@
/* High Contrast Theming */
.hc-black .monaco-workbench .search-viewlet .foldermatch,
.hc-black .monaco-workbench .search-viewlet .filematch,
.hc-black .monaco-workbench .search-viewlet .linematch {
line-height: 20px;
......
......@@ -15,7 +15,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { INavigator } from 'vs/base/common/iterator';
import { SearchViewlet } from 'vs/workbench/parts/search/browser/searchViewlet';
import { SearchResult, Match, FileMatch, FileMatchOrMatch } from 'vs/workbench/parts/search/common/searchModel';
import { Match, FileMatch, FileMatchOrMatch, FolderMatch } from 'vs/workbench/parts/search/common/searchModel';
import { IReplaceService } from 'vs/workbench/parts/search/common/replace';
import * as Constants from 'vs/workbench/parts/search/common/constants';
import { CollapseAllAction as TreeCollapseAction } from 'vs/base/parts/tree/browser/treeDefaults';
......@@ -421,7 +421,7 @@ export class RemoveAction extends AbstractSearchAndReplaceAction {
let elementToRefresh: any;
if (this.element instanceof FileMatch) {
let parent: SearchResult = <SearchResult>this.element.parent();
let parent: FolderMatch = <FolderMatch>this.element.parent();
parent.remove(<FileMatch>this.element);
elementToRefresh = parent;
} else {
......
......@@ -13,7 +13,7 @@ import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
import { FileLabel } from 'vs/workbench/browser/labels';
import { ITree, IDataSource, ISorter, IAccessibilityProvider, IFilter, IRenderer } from 'vs/base/parts/tree/browser/tree';
import { Match, SearchResult, FileMatch, FileMatchOrMatch, SearchModel } from 'vs/workbench/parts/search/common/searchModel';
import { Match, SearchResult, FileMatch, FileMatchOrMatch, SearchModel, FolderMatch } from 'vs/workbench/parts/search/common/searchModel';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { Range } from 'vs/editor/common/core/range';
import { SearchViewlet } from 'vs/workbench/parts/search/browser/searchViewlet';
......@@ -22,12 +22,19 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { attachBadgeStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { getPathLabel } from 'vs/base/common/labels';
import { FileKind } from "vs/platform/files/common/files";
export class SearchDataSource implements IDataSource {
private static AUTOEXPAND_CHILD_LIMIT = 10;
constructor(private includeFolderMatch: boolean = true) { }
public getId(tree: ITree, element: any): string {
if (element instanceof FolderMatch) {
return element.id();
}
if (element instanceof FileMatch) {
return element.id();
}
......@@ -42,8 +49,14 @@ export class SearchDataSource implements IDataSource {
private _getChildren(element: any): any[] {
if (element instanceof FileMatch) {
return element.matches();
} else if (element instanceof SearchResult) {
} else if (element instanceof FolderMatch) {
return element.matches();
} else if (element instanceof SearchResult) {
if (this.includeFolderMatch) {
return element.folderMatches().filter(fm => !fm.isEmpty());
} else {
return element.matches();
}
}
return [];
......@@ -54,7 +67,7 @@ export class SearchDataSource implements IDataSource {
}
public hasChildren(tree: ITree, element: any): boolean {
return element instanceof FileMatch || element instanceof SearchResult;
return element instanceof FileMatch || element instanceof FolderMatch || element instanceof SearchResult;
}
public getParent(tree: ITree, element: any): TPromise<any> {
......@@ -63,6 +76,8 @@ export class SearchDataSource implements IDataSource {
if (element instanceof Match) {
value = element.parent();
} else if (element instanceof FileMatch) {
value = this.includeFolderMatch ? element.parent() : element.parent().parent();
} else if (element instanceof FolderMatch) {
value = element.parent();
}
......@@ -71,13 +86,20 @@ export class SearchDataSource implements IDataSource {
public shouldAutoexpand(tree: ITree, element: any): boolean {
const numChildren = this._getChildren(element).length;
return numChildren > 0 && numChildren < SearchDataSource.AUTOEXPAND_CHILD_LIMIT;
if (numChildren <= 0) {
return false;
}
return numChildren < SearchDataSource.AUTOEXPAND_CHILD_LIMIT || element instanceof FolderMatch;
}
}
export class SearchSorter implements ISorter {
public compare(tree: ITree, elementA: FileMatchOrMatch, elementB: FileMatchOrMatch): number {
if (elementA instanceof FolderMatch && elementB instanceof FolderMatch) {
return elementA.index() - elementB.index();
}
if (elementA instanceof FileMatch && elementB instanceof FileMatch) {
return elementA.resource().fsPath.localeCompare(elementB.resource().fsPath) || elementA.name().localeCompare(elementB.name());
}
......@@ -90,6 +112,11 @@ export class SearchSorter implements ISorter {
}
}
interface IFolderMatchTemplate {
label: FileLabel;
badge: CountBadge;
}
interface IFileMatchTemplate {
label: FileLabel;
badge: CountBadge;
......@@ -107,6 +134,7 @@ interface IMatchTemplate {
export class SearchRenderer extends Disposable implements IRenderer {
private static FOLDER_MATCH_TEMPLATE_ID = 'folderMatch';
private static FILE_MATCH_TEMPLATE_ID = 'fileMatch';
private static MATCH_TEMPLATE_ID = 'match';
......@@ -125,7 +153,9 @@ export class SearchRenderer extends Disposable implements IRenderer {
}
public getTemplateId(tree: ITree, element: any): string {
if (element instanceof FileMatch) {
if (element instanceof FolderMatch) {
return SearchRenderer.FOLDER_MATCH_TEMPLATE_ID;
} else if (element instanceof FileMatch) {
return SearchRenderer.FILE_MATCH_TEMPLATE_ID;
} else if (element instanceof Match) {
return SearchRenderer.MATCH_TEMPLATE_ID;
......@@ -134,6 +164,10 @@ export class SearchRenderer extends Disposable implements IRenderer {
}
public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any {
if (templateId === SearchRenderer.FOLDER_MATCH_TEMPLATE_ID) {
return this.renderFolderMatchTemplate(tree, templateId, container);
}
if (templateId === SearchRenderer.FILE_MATCH_TEMPLATE_ID) {
return this.renderFileMatchTemplate(tree, templateId, container);
}
......@@ -146,13 +180,23 @@ export class SearchRenderer extends Disposable implements IRenderer {
}
public renderElement(tree: ITree, element: any, templateId: string, templateData: any): void {
if (SearchRenderer.FILE_MATCH_TEMPLATE_ID === templateId) {
if (SearchRenderer.FOLDER_MATCH_TEMPLATE_ID === templateId) {
this.renderFolderMatch(tree, <FolderMatch>element, <IFolderMatchTemplate>templateData);
} else if (SearchRenderer.FILE_MATCH_TEMPLATE_ID === templateId) {
this.renderFileMatch(tree, <FileMatch>element, <IFileMatchTemplate>templateData);
} else if (SearchRenderer.MATCH_TEMPLATE_ID === templateId) {
this.renderMatch(tree, <Match>element, <IMatchTemplate>templateData);
}
}
private renderFolderMatchTemplate(tree: ITree, templateId: string, container: HTMLElement): IFolderMatchTemplate {
let folderMatchElement = DOM.append(container, DOM.$('.foldermatch'));
const label = this.instantiationService.createInstance(FileLabel, folderMatchElement, void 0);
const badge = new CountBadge(DOM.append(folderMatchElement, DOM.$('.badge')));
this._register(attachBadgeStyler(badge, this.themeService));
return { label, badge };
}
private renderFileMatchTemplate(tree: ITree, templateId: string, container: HTMLElement): IFileMatchTemplate {
let fileMatchElement = DOM.append(container, DOM.$('.filematch'));
const label = this.instantiationService.createInstance(FileLabel, fileMatchElement, void 0);
......@@ -182,8 +226,21 @@ export class SearchRenderer extends Disposable implements IRenderer {
};
}
private renderFolderMatch(tree: ITree, folderMatch: FolderMatch, templateData: IFolderMatchTemplate): void {
if (folderMatch.hasRoot()) {
templateData.label.setFile(folderMatch.resource(), { fileKind: FileKind.ROOT_FOLDER });
} else {
templateData.label.setValue(nls.localize('searchFolderMatch.other.label', "Other files"));
}
let count = folderMatch.fileCount();
templateData.badge.setCount(count);
templateData.badge.setTitleFormat(count > 1 ? nls.localize('searchFileMatches', "{0} files found", count) : nls.localize('searchFileMatch', "{0} file found", count));
}
private renderFileMatch(tree: ITree, fileMatch: FileMatch, templateData: IFileMatchTemplate): void {
templateData.label.setFile(fileMatch.resource());
const folderMatch = fileMatch.parent();
const root = folderMatch.hasRoot() ? folderMatch.resource() : undefined;
templateData.label.setFile(fileMatch.resource(), { root });
let count = fileMatch.count();
templateData.badge.setCount(count);
templateData.badge.setTitleFormat(count > 1 ? nls.localize('searchMatches', "{0} matches found", count) : nls.localize('searchMatch', "{0} match found", count));
......@@ -220,6 +277,9 @@ export class SearchRenderer extends Disposable implements IRenderer {
}
public disposeTemplate(tree: ITree, templateId: string, templateData: any): void {
if (SearchRenderer.FOLDER_MATCH_TEMPLATE_ID === templateId) {
(<IFolderMatchTemplate>templateData).label.dispose();
}
if (SearchRenderer.FILE_MATCH_TEMPLATE_ID === templateId) {
(<IFileMatchTemplate>templateData).label.dispose();
}
......@@ -232,6 +292,10 @@ export class SearchAccessibilityProvider implements IAccessibilityProvider {
}
public getAriaLabel(tree: ITree, element: FileMatchOrMatch): string {
if (element instanceof FolderMatch) {
return nls.localize('folderMatchAriaLabel', "{0} matches in folder root {1}, Search result", element.count(), element.name());
}
if (element instanceof FileMatch) {
const path = getPathLabel(element.resource(), this.contextService) || element.resource().fsPath;
......@@ -256,6 +320,6 @@ export class SearchAccessibilityProvider implements IAccessibilityProvider {
export class SearchFilter implements IFilter {
public isVisible(tree: ITree, element: any): boolean {
return !(element instanceof FileMatch) || element.matches().length > 0;
return !(element instanceof FileMatch || element instanceof FolderMatch) || element.matches().length > 0;
}
}
\ No newline at end of file
}
......@@ -442,7 +442,7 @@ export class SearchViewlet extends Viewlet {
this.results = div;
this.results.addClass('show-file-icons');
let dataSource = new SearchDataSource();
let dataSource = new SearchDataSource(this.contextService.hasMultiFolderWorkspace());
let renderer = this.instantiationService.createInstance(SearchRenderer, this.getActionRunner(), this);
this.tree = new Tree(div.getHTMLElement(), {
......@@ -1168,7 +1168,7 @@ export class SearchViewlet extends Viewlet {
const msgWasHidden = this.messages.isHidden();
if (fileCount > 0) {
const div = this.clearMessage();
$(div).p({ text: this.buildResultCountMessage(this.viewModel.searchResult.count(), fileCount) });
$(div).p({ text: this.buildResultCountMessage(this.viewModel.searchResult.count(), fileCount, this.viewModel.searchResult.folderCount()) });
if (msgWasHidden) {
this.reLayout();
}
......@@ -1177,15 +1177,27 @@ export class SearchViewlet extends Viewlet {
}
}
private buildResultCountMessage(resultCount: number, fileCount: number): string {
if (resultCount === 1 && fileCount === 1) {
return nls.localize('search.file.result', "{0} result in {1} file", resultCount, fileCount);
} else if (resultCount === 1) {
return nls.localize('search.files.result', "{0} result in {1} files", resultCount, fileCount);
} else if (fileCount === 1) {
return nls.localize('search.file.results', "{0} results in {1} file", resultCount, fileCount);
private buildResultCountMessage(resultCount: number, fileCount: number, folderCount: number): string {
if (folderCount === 1) {
if (resultCount === 1 && fileCount === 1) {
return nls.localize('search.folder.file.result', "{0} result in {1} file in {2} folder", resultCount, fileCount, folderCount);
} else if (resultCount === 1) {
return nls.localize('search.folder.files.result', "{0} result in {1} files in {2} folder", resultCount, fileCount, folderCount);
} else if (fileCount === 1) {
return nls.localize('search.folder.file.results', "{0} results in {1} file in {2} folder", resultCount, fileCount, folderCount);
} else {
return nls.localize('search.folder.files.results', "{0} results in {1} files in {2} folder", resultCount, fileCount, folderCount);
}
} else {
return nls.localize('search.files.results', "{0} results in {1} files", resultCount, fileCount);
if (resultCount === 1 && fileCount === 1) {
return nls.localize('search.folders.file.result', "{0} result in {1} file in {2} folders", resultCount, fileCount, folderCount);
} else if (resultCount === 1) {
return nls.localize('search.folders.files.result', "{0} result in {1} files in {2} folders", resultCount, fileCount, folderCount);
} else if (fileCount === 1) {
return nls.localize('search.folders.file.results', "{0} results in {1} file in {2} folders", resultCount, fileCount, folderCount);
} else {
return nls.localize('search.folders.files.results', "{0} results in {1} files in {2} folders", resultCount, fileCount, folderCount);
}
}
}
......
......@@ -11,7 +11,7 @@ import { RunOnceScheduler } from 'vs/base/common/async';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { TPromise, PPromise } from 'vs/base/common/winjs.base';
import URI from 'vs/base/common/uri';
import { values, ResourceMap } from 'vs/base/common/map';
import { values, ResourceMap, TrieMap } from 'vs/base/common/map';
import Event, { Emitter, fromPromise, stopwatch, any } from 'vs/base/common/event';
import { ISearchService, ISearchProgressItem, ISearchComplete, ISearchQuery, IPatternInfo, IFileMatch } from 'vs/platform/search/common/search';
import { ReplacePattern } from 'vs/platform/search/common/replace';
......@@ -132,7 +132,7 @@ export class FileMatch extends Disposable {
private _updateScheduler: RunOnceScheduler;
private _modelDecorations: string[] = [];
constructor(private _query: IPatternInfo, private _maxResults: number, private _parent: SearchResult, private rawMatch: IFileMatch,
constructor(private _query: IPatternInfo, private _maxResults: number, private _parent: FolderMatch, private rawMatch: IFileMatch,
@IModelService private modelService: IModelService, @IReplaceService private replaceService: IReplaceService) {
super();
this._resource = this.rawMatch.resource;
......@@ -252,7 +252,7 @@ export class FileMatch extends Disposable {
return this.resource().toString();
}
public parent(): SearchResult {
public parent(): FolderMatch {
return this._parent;
}
......@@ -334,45 +334,66 @@ export interface IChangeEvent {
removed?: boolean;
}
export class SearchResult extends Disposable {
export class FolderMatch extends Disposable {
private _onChange = this._register(new Emitter<IChangeEvent>());
public onChange: Event<IChangeEvent> = this._onChange.event;
private _onDispose = this._register(new Emitter<void>());
public onDispose: Event<void> = this._onDispose.event;
private _fileMatches: ResourceMap<FileMatch>;
private _unDisposedFileMatches: ResourceMap<FileMatch>;
private _query: IPatternInfo = null;
private _maxResults: number;
private _showHighlights: boolean;
private _replacingAll: boolean = false;
private _rangeHighlightDecorations: RangeHighlightDecorations;
constructor(private _searchModel: SearchModel, @IReplaceService private replaceService: IReplaceService, @ITelemetryService private telemetryService: ITelemetryService,
constructor(private _resource: URI, private _id: string, private _index: number, private _query: ISearchQuery, private _parent: SearchResult, private _searchModel: SearchModel, @IReplaceService private replaceService: IReplaceService, @ITelemetryService private telemetryService: ITelemetryService,
@IInstantiationService private instantiationService: IInstantiationService) {
super();
this._fileMatches = new ResourceMap<FileMatch>();
this._unDisposedFileMatches = new ResourceMap<FileMatch>();
this._rangeHighlightDecorations = this.instantiationService.createInstance(RangeHighlightDecorations);
}
public set query(query: IPatternInfo) {
this._query = query;
public get searchModel(): SearchModel {
return this._searchModel;
}
public set maxResults(maxResults: number) {
this._maxResults = maxResults;
public get showHighlights(): boolean {
return this._parent.showHighlights;
}
public get searchModel(): SearchModel {
return this._searchModel;
public set replacingAll(b: boolean) {
this._replacingAll = b;
}
public id(): string {
return this._id;
}
public resource(): URI {
return this._resource;
}
public add(raw: IFileMatch[], silent: boolean = false): void {
public index(): number {
return this._index;
}
public name(): string {
return paths.basename(this.resource().fsPath);
}
public parent(): SearchResult {
return this._parent;
}
public hasRoot(): boolean {
return this._resource.fsPath !== '';
}
public add(raw: IFileMatch[], silent: boolean): void {
let changed: FileMatch[] = [];
raw.forEach((rawFileMatch) => {
if (!this._fileMatches.has(rawFileMatch.resource)) {
let fileMatch = this.instantiationService.createInstance(FileMatch, this._query, this._maxResults, this, rawFileMatch);
let fileMatch = this.instantiationService.createInstance(FileMatch, this._query.contentPattern, this._query.maxResults, this, rawFileMatch);
this.doAdd(fileMatch);
changed.push(fileMatch);
let disposable = fileMatch.onChange(() => this.onFileChange(fileMatch));
......@@ -400,31 +421,179 @@ export class SearchResult extends Disposable {
});
}
public matches(): FileMatch[] {
return this._fileMatches.values();
}
public isEmpty(): boolean {
return this.fileCount() === 0;
}
public fileCount(): number {
return this._fileMatches.size;
}
public count(): number {
return this.matches().reduce<number>((prev, match) => prev + match.count(), 0);
}
private onFileChange(fileMatch: FileMatch): void {
let added: boolean = false;
let removed: boolean = false;
if (!this._fileMatches.has(fileMatch.resource())) {
this.doAdd(fileMatch);
added = true;
}
if (fileMatch.count() === 0) {
this.doRemove(fileMatch, false, false);
added = false;
removed = true;
}
if (!this._replacingAll) {
this._onChange.fire({ elements: [fileMatch], added: added, removed: removed });
}
}
private doAdd(fileMatch: FileMatch): void {
this._fileMatches.set(fileMatch.resource(), fileMatch);
if (this._unDisposedFileMatches.has(fileMatch.resource())) {
this._unDisposedFileMatches.delete(fileMatch.resource());
}
}
private doRemove(fileMatch: FileMatch, dispose: boolean = true, trigger: boolean = true): void {
this._fileMatches.delete(fileMatch.resource());
if (dispose) {
fileMatch.dispose();
} else {
this._unDisposedFileMatches.set(fileMatch.resource(), fileMatch);
}
if (trigger) {
this._onChange.fire({ elements: [fileMatch], removed: true });
}
}
private disposeMatches(): void {
this._fileMatches.values().forEach((fileMatch: FileMatch) => fileMatch.dispose());
this._unDisposedFileMatches.values().forEach((fileMatch: FileMatch) => fileMatch.dispose());
this._fileMatches.clear();
this._unDisposedFileMatches.clear();
}
public dispose(): void {
this.disposeMatches();
this._onDispose.fire();
super.dispose();
}
}
export class SearchResult extends Disposable {
private _onChange = this._register(new Emitter<IChangeEvent>());
public onChange: Event<IChangeEvent> = this._onChange.event;
private _folderMatches: FolderMatch[] = [];
private _folderMatchesMap: TrieMap<FolderMatch> = new TrieMap<FolderMatch>(TrieMap.PathSplitter);
private _query: ISearchQuery = null;
private _showHighlights: boolean;
private _rangeHighlightDecorations: RangeHighlightDecorations;
constructor(private _searchModel: SearchModel, @IReplaceService private replaceService: IReplaceService, @ITelemetryService private telemetryService: ITelemetryService,
@IInstantiationService private instantiationService: IInstantiationService) {
super();
this._rangeHighlightDecorations = this.instantiationService.createInstance(RangeHighlightDecorations);
}
public set query(query: ISearchQuery) {
// When updating the query we could change the roots, so ensure we clean up the old roots first.
this.clear();
this._query = query;
const otherFiles = URI.parse('');
this._folderMatches = (query.folderQueries || []).map((fq) => fq.folder).concat([otherFiles]).map((resource, index) => {
const id = resource.toString() || 'otherFiles';
const folderMatch = this.instantiationService.createInstance(FolderMatch, resource, id, index, query, this, this._searchModel);
const disposable = folderMatch.onChange((event) => this._onChange.fire(event));
folderMatch.onDispose(() => disposable.dispose());
return folderMatch;
});
// otherFiles is the fallback for missing values in the TrieMap. So we do not insert it.
this._folderMatches.slice(0, this.folderMatches.length - 1)
.forEach(fm => this._folderMatchesMap.insert(fm.resource().fsPath, fm));
}
public get searchModel(): SearchModel {
return this._searchModel;
}
public add(allRaw: IFileMatch[], silent: boolean = false): void {
// Split up raw into a list per folder so we can do a batch add per folder.
let rawPerFolder = new ResourceMap<IFileMatch[]>();
this._folderMatches.forEach((folderMatch) => rawPerFolder.set(folderMatch.resource(), []));
allRaw.forEach(rawFileMatch => {
let folderMatch = this.getFolderMatch(rawFileMatch.resource);
rawPerFolder.get(folderMatch.resource()).push(rawFileMatch);
});
rawPerFolder.forEach((raw) => {
if (!raw.length) {
return;
}
let folderMatch = this.getFolderMatch(raw[0].resource);
folderMatch.add(raw, silent);
});
}
public clear(): void {
this._folderMatches.forEach((folderMatch) => folderMatch.clear());
this.disposeMatches();
}
public remove(match: FileMatch): void {
this.getFolderMatch(match.resource()).remove(match);
}
public replace(match: FileMatch): TPromise<any> {
return this.getFolderMatch(match.resource()).replace(match);
}
public replaceAll(progressRunner: IProgressRunner): TPromise<any> {
this._replacingAll = true;
this.replacingAll = true;
const promise = this.replaceService.replace(this.matches(), progressRunner);
const onDone = stopwatch(fromPromise(promise));
onDone(duration => this.telemetryService.publicLog('replaceAll.started', { duration }));
return promise.then(() => {
this._replacingAll = false;
this.replacingAll = false;
this.clear();
}, () => {
this._replacingAll = false;
this.replacingAll = false;
});
}
public folderMatches(): FolderMatch[] {
return this._folderMatches.concat();
}
public matches(): FileMatch[] {
return this._fileMatches.values();
let matches: FileMatch[][] = [];
this._folderMatches.forEach((folderMatch) => {
matches.push(folderMatch.matches());
});
return [].concat(...matches);
}
public isEmpty(): boolean {
return this.fileCount() === 0;
return this._folderMatches.every((folderMatch) => folderMatch.isEmpty());
}
public fileCount(): number {
return this._fileMatches.size;
return this.folderMatches().reduce<number>((prev, match) => prev + match.fileCount(), 0);
}
public folderCount(): number {
return this.folderMatches().reduce<number>((prev, match) => prev + (match.fileCount() > 0 ? 1 : 0), 0);
}
public count(): number {
......@@ -461,48 +630,25 @@ export class SearchResult extends Disposable {
return this._rangeHighlightDecorations;
}
private onFileChange(fileMatch: FileMatch): void {
let added: boolean = false;
let removed: boolean = false;
if (!this._fileMatches.has(fileMatch.resource())) {
this.doAdd(fileMatch);
added = true;
}
if (fileMatch.count() === 0) {
this.doRemove(fileMatch, false, false);
added = false;
removed = true;
}
if (!this._replacingAll) {
this._onChange.fire({ elements: [fileMatch], added: added, removed: removed });
}
private getFolderMatch(resource: URI): FolderMatch {
const folderMatch = this._folderMatchesMap.findSubstr(resource.fsPath);
return folderMatch ? folderMatch : this.otherFiles;
}
private doAdd(fileMatch: FileMatch): void {
this._fileMatches.set(fileMatch.resource(), fileMatch);
if (this._unDisposedFileMatches.has(fileMatch.resource())) {
this._unDisposedFileMatches.delete(fileMatch.resource());
}
private get otherFiles(): FolderMatch {
return this._folderMatches[this._folderMatches.length - 1];
}
private doRemove(fileMatch: FileMatch, dispose: boolean = true, trigger: boolean = true): void {
this._fileMatches.delete(fileMatch.resource());
if (dispose) {
fileMatch.dispose();
} else {
this._unDisposedFileMatches.set(fileMatch.resource(), fileMatch);
}
if (trigger) {
this._onChange.fire({ elements: [fileMatch], removed: true });
}
private set replacingAll(running: boolean) {
this._folderMatches.forEach((folderMatch) => {
folderMatch.replacingAll = running;
});
}
private disposeMatches(): void {
this._fileMatches.values().forEach((fileMatch: FileMatch) => fileMatch.dispose());
this._unDisposedFileMatches.values().forEach((fileMatch: FileMatch) => fileMatch.dispose());
this._fileMatches.clear();
this._unDisposedFileMatches.clear();
this._folderMatches.forEach(folderMatch => folderMatch.dispose());
this._folderMatches = [];
this._folderMatchesMap = new TrieMap<FolderMatch>(TrieMap.PathSplitter);
this._rangeHighlightDecorations.removeHighlightRange();
}
......@@ -566,8 +712,7 @@ export class SearchModel extends Disposable {
this.searchResult.clear();
this._searchResult.query = this._searchQuery.contentPattern;
this._searchResult.maxResults = this._searchQuery.maxResults;
this._searchResult.query = this._searchQuery;
this._replacePattern = new ReplacePattern(this._replaceString, this._searchQuery.contentPattern);
const onDone = fromPromise(this.currentRequest);
......
......@@ -25,7 +25,8 @@ suite('Search - Viewlet', () => {
test('Data Source', function () {
let ds = new SearchDataSource();
let result = instantiation.createInstance(SearchResult, null);
let result: SearchResult = instantiation.createInstance(SearchResult, null);
result.query = { type: 1, folderQueries: [{ folder: uri.parse('file://c:/') }] };
result.add([{
resource: uri.parse('file:///c:/foo'),
lineMatches: [{ lineNumber: 1, preview: 'bar', offsetAndLengths: [[0, 1]] }]
......
......@@ -11,7 +11,7 @@ import { DeferredPPromise } from 'vs/base/test/common/utils';
import { PPromise } from 'vs/base/common/winjs.base';
import { SearchModel } from 'vs/workbench/parts/search/common/searchModel';
import URI from 'vs/base/common/uri';
import { IFileMatch, ILineMatch, ISearchService, ISearchComplete, ISearchProgressItem, IUncachedSearchStats } from 'vs/platform/search/common/search';
import { IFileMatch, IFolderQuery, ILineMatch, ISearchService, ISearchComplete, ISearchProgressItem, IUncachedSearchStats } from 'vs/platform/search/common/search';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { Range } from 'vs/editor/common/core/range';
......@@ -57,6 +57,10 @@ suite('SearchModel', () => {
filesWalked: 3
};
const folderQueries: IFolderQuery[] = [
{ folder: URI.parse('file://c:/') }
];
setup(() => {
restoreStubs = [];
instantiationService = new TestInstantiationService();
......@@ -76,8 +80,8 @@ suite('SearchModel', () => {
let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]])), aRawMatch('file://c:/2', aLineMatch('preview 2'))];
instantiationService.stub(ISearchService, 'search', PPromise.as({ results: results }));
let testObject = instantiationService.createInstance(SearchModel);
testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1 });
let testObject: SearchModel = instantiationService.createInstance(SearchModel);
testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
let actual = testObject.searchResult.matches();
......@@ -103,7 +107,7 @@ suite('SearchModel', () => {
instantiationService.stub(ISearchService, 'search', promise);
let testObject = instantiationService.createInstance(SearchModel);
let result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1 });
let result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
promise.progress(results[0]);
promise.progress(results[1]);
......@@ -137,7 +141,7 @@ suite('SearchModel', () => {
instantiationService.stub(ISearchService, 'search', PPromise.as({ results: results }));
let testObject = instantiationService.createInstance(SearchModel);
testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1 });
testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
assert.ok(target.calledOnce);
const data = target.args[0];
......@@ -154,7 +158,7 @@ suite('SearchModel', () => {
instantiationService.stub(ISearchService, 'search', PPromise.as({ results: [] }));
let testObject = instantiationService.createInstance(SearchModel);
const result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1 });
const result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
setTimeout(() => {
result.done(() => {
......@@ -176,7 +180,7 @@ suite('SearchModel', () => {
instantiationService.stub(ISearchService, 'search', promise);
let testObject = instantiationService.createInstance(SearchModel);
let result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1 });
let result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
promise.progress(aRawMatch('file://c:/1', aLineMatch('some preview')));
promise.complete({ results: [], stats: testSearchStats });
......@@ -202,7 +206,7 @@ suite('SearchModel', () => {
instantiationService.stub(ISearchService, 'search', promise);
let testObject = instantiationService.createInstance(SearchModel);
let result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1 });
let result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
promise.error('error');
......@@ -227,7 +231,7 @@ suite('SearchModel', () => {
instantiationService.stub(ISearchService, 'search', promise);
let testObject = instantiationService.createInstance(SearchModel);
let result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1 });
let result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
promise.cancel();
......@@ -245,12 +249,12 @@ suite('SearchModel', () => {
let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]])), aRawMatch('file://c:/2', aLineMatch('preview 2'))];
instantiationService.stub(ISearchService, 'search', PPromise.as({ results: results }));
let testObject: SearchModel = instantiationService.createInstance(SearchModel);
testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1 });
testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
assert.ok(!testObject.searchResult.isEmpty());
instantiationService.stub(ISearchService, 'search', new DeferredPPromise<ISearchComplete, ISearchProgressItem>());
testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1 });
testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
assert.ok(testObject.searchResult.isEmpty());
});
......@@ -259,9 +263,9 @@ suite('SearchModel', () => {
instantiationService.stub(ISearchService, 'search', new DeferredPPromise((c, e, p) => { }, target));
let testObject: SearchModel = instantiationService.createInstance(SearchModel);
testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1 });
testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
instantiationService.stub(ISearchService, 'search', new DeferredPPromise<ISearchComplete, ISearchProgressItem>());
testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1 });
testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries });
assert.ok(target.calledOnce);
});
......@@ -271,24 +275,24 @@ suite('SearchModel', () => {
instantiationService.stub(ISearchService, 'search', PPromise.as({ results: results }));
let testObject: SearchModel = instantiationService.createInstance(SearchModel);
testObject.search({ contentPattern: { pattern: 're' }, type: 1 });
testObject.search({ contentPattern: { pattern: 're' }, type: 1, folderQueries });
testObject.replaceString = 'hello';
let match = testObject.searchResult.matches()[0].matches()[0];
assert.equal('hello', match.replaceString);
testObject.search({ contentPattern: { pattern: 're', isRegExp: true }, type: 1 });
testObject.search({ contentPattern: { pattern: 're', isRegExp: true }, type: 1, folderQueries });
match = testObject.searchResult.matches()[0].matches()[0];
assert.equal('hello', match.replaceString);
testObject.search({ contentPattern: { pattern: 're(?:vi)', isRegExp: true }, type: 1 });
testObject.search({ contentPattern: { pattern: 're(?:vi)', isRegExp: true }, type: 1, folderQueries });
match = testObject.searchResult.matches()[0].matches()[0];
assert.equal('hello', match.replaceString);
testObject.search({ contentPattern: { pattern: 'r(e)(?:vi)', isRegExp: true }, type: 1 });
testObject.search({ contentPattern: { pattern: 'r(e)(?:vi)', isRegExp: true }, type: 1, folderQueries });
match = testObject.searchResult.matches()[0].matches()[0];
assert.equal('hello', match.replaceString);
testObject.search({ contentPattern: { pattern: 'r(e)(?:vi)', isRegExp: true }, type: 1 });
testObject.search({ contentPattern: { pattern: 'r(e)(?:vi)', isRegExp: true }, type: 1, folderQueries });
testObject.replaceString = 'hello$1';
match = testObject.searchResult.matches()[0].matches()[0];
assert.equal('helloe', match.replaceString);
......
......@@ -368,6 +368,7 @@ suite('SearchResult', () => {
function aSearchResult(): SearchResult {
let searchModel = instantiationService.createInstance(SearchModel);
searchModel.searchResult.query = { type: 1, folderQueries: [{ folder: URI.parse('file://c:/') }] };
return searchModel.searchResult;
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册