searchResultsView.ts 13.4 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

S
Sandeep Somavarapu 已提交
6 7 8
import * as nls from 'vs/nls';
import * as paths from 'vs/base/common/paths';
import * as DOM from 'vs/base/browser/dom';
9
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
10 11
import { TPromise } from 'vs/base/common/winjs.base';
import { IAction, IActionRunner } from 'vs/base/common/actions';
S
Sandeep Somavarapu 已提交
12
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
13
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
14
import { FileLabel } from 'vs/workbench/browser/labels';
S
Sandeep Somavarapu 已提交
15
import { ITree, IDataSource, ISorter, IAccessibilityProvider, IFilter, IRenderer } from 'vs/base/parts/tree/browser/tree';
16
import { Match, SearchResult, FileMatch, FileMatchOrMatch, SearchModel, FolderMatch } from 'vs/workbench/parts/search/common/searchModel';
17
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
18
import { Range } from 'vs/editor/common/core/range';
I
isidor 已提交
19
import { SearchView } from 'vs/workbench/parts/search/browser/searchView';
20
import { RemoveAction, ReplaceAllAction, ReplaceAction, ReplaceAllInFolderAction } from 'vs/workbench/parts/search/browser/searchActions';
21
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
22 23
import { attachBadgeStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
B
Benjamin Pasero 已提交
24
import { getPathLabel } from 'vs/base/common/labels';
B
Benjamin Pasero 已提交
25
import { FileKind } from 'vs/platform/files/common/files';
26 27 28

export class SearchDataSource implements IDataSource {

29
	private static readonly AUTOEXPAND_CHILD_LIMIT = 10;
30

31 32 33 34 35 36 37 38 39 40 41
	private includeFolderMatch: boolean;
	private listener: IDisposable;

	constructor( @IWorkspaceContextService private contextService: IWorkspaceContextService) {
		this.updateIncludeFolderMatch();
		this.listener = this.contextService.onDidChangeWorkbenchState(() => this.updateIncludeFolderMatch());
	}

	private updateIncludeFolderMatch(): void {
		this.includeFolderMatch = (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE);
	}
42

43
	public getId(tree: ITree, element: any): string {
44 45 46 47
		if (element instanceof FolderMatch) {
			return element.id();
		}

48 49 50 51 52 53 54 55 56 57 58
		if (element instanceof FileMatch) {
			return element.id();
		}

		if (element instanceof Match) {
			return element.id();
		}

		return 'root';
	}

59
	private _getChildren(element: any): any[] {
60
		if (element instanceof FileMatch) {
61
			return element.matches();
62
		} else if (element instanceof FolderMatch) {
63
			return element.matches();
64
		} else if (element instanceof SearchResult) {
65 66 67 68
			const folderMatches = element.folderMatches();
			return folderMatches.length > 2 ? // "Other files" + workspace folder = 2
				folderMatches.filter(fm => !fm.isEmpty()) :
				element.matches();
69 70
		}

71 72 73 74 75
		return [];
	}

	public getChildren(tree: ITree, element: any): TPromise<any[]> {
		return TPromise.as(this._getChildren(element));
76 77 78
	}

	public hasChildren(tree: ITree, element: any): boolean {
79
		return element instanceof FileMatch || element instanceof FolderMatch || element instanceof SearchResult;
80 81 82 83 84 85 86 87
	}

	public getParent(tree: ITree, element: any): TPromise<any> {
		let value: any = null;

		if (element instanceof Match) {
			value = element.parent();
		} else if (element instanceof FileMatch) {
88
			value = this.includeFolderMatch ? element.parent() : element.parent().parent();
89 90
		} else if (element instanceof FolderMatch) {
			value = element.parent();
91 92 93 94
		}

		return TPromise.as(value);
	}
95 96 97

	public shouldAutoexpand(tree: ITree, element: any): boolean {
		const numChildren = this._getChildren(element).length;
98 99 100 101
		if (numChildren <= 0) {
			return false;
		}
		return numChildren < SearchDataSource.AUTOEXPAND_CHILD_LIMIT || element instanceof FolderMatch;
102
	}
103 104 105 106

	public dispose(): void {
		this.listener = dispose(this.listener);
	}
107 108 109 110 111
}

export class SearchSorter implements ISorter {

	public compare(tree: ITree, elementA: FileMatchOrMatch, elementB: FileMatchOrMatch): number {
112
		if (elementA instanceof FolderMatch && elementB instanceof FolderMatch) {
113
			return elementA.index() - elementB.index();
114 115
		}

116
		if (elementA instanceof FileMatch && elementB instanceof FileMatch) {
117
			return elementA.resource().fsPath.localeCompare(elementB.resource().fsPath) || elementA.name().localeCompare(elementB.name());
118 119 120 121 122
		}

		if (elementA instanceof Match && elementB instanceof Match) {
			return Range.compareRangesUsingStarts(elementA.range(), elementB.range());
		}
M
Matt Bierner 已提交
123 124

		return undefined;
125 126 127
	}
}

128 129 130
interface IFolderMatchTemplate {
	label: FileLabel;
	badge: CountBadge;
131
	actions: ActionBar;
132 133
}

S
Sandeep Somavarapu 已提交
134 135 136
interface IFileMatchTemplate {
	label: FileLabel;
	badge: CountBadge;
S
Sandeep Somavarapu 已提交
137
	actions: ActionBar;
S
Sandeep Somavarapu 已提交
138 139 140 141 142 143
}

interface IMatchTemplate {
	parent: HTMLElement;
	before: HTMLElement;
	match: HTMLElement;
S
Sandeep Somavarapu 已提交
144
	replace: HTMLElement;
S
Sandeep Somavarapu 已提交
145
	after: HTMLElement;
S
Sandeep Somavarapu 已提交
146
	actions: ActionBar;
S
Sandeep Somavarapu 已提交
147 148 149 150
}

export class SearchRenderer extends Disposable implements IRenderer {

151 152 153
	private static readonly FOLDER_MATCH_TEMPLATE_ID = 'folderMatch';
	private static readonly FILE_MATCH_TEMPLATE_ID = 'fileMatch';
	private static readonly MATCH_TEMPLATE_ID = 'match';
154

155 156
	constructor(
		actionRunner: IActionRunner,
157
		private searchView: SearchView,
158 159 160
		@IInstantiationService private instantiationService: IInstantiationService,
		@IThemeService private themeService: IThemeService
	) {
S
Sandeep Somavarapu 已提交
161
		super();
162 163
	}

S
Sandeep Somavarapu 已提交
164
	public getHeight(tree: ITree, element: any): number {
165 166 167
		return 22;
	}

S
Sandeep Somavarapu 已提交
168
	public getTemplateId(tree: ITree, element: any): string {
169 170 171
		if (element instanceof FolderMatch) {
			return SearchRenderer.FOLDER_MATCH_TEMPLATE_ID;
		} else if (element instanceof FileMatch) {
S
Sandeep Somavarapu 已提交
172 173 174 175 176 177
			return SearchRenderer.FILE_MATCH_TEMPLATE_ID;
		} else if (element instanceof Match) {
			return SearchRenderer.MATCH_TEMPLATE_ID;
		}
		return null;
	}
178

S
Sandeep Somavarapu 已提交
179
	public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any {
180 181 182 183
		if (templateId === SearchRenderer.FOLDER_MATCH_TEMPLATE_ID) {
			return this.renderFolderMatchTemplate(tree, templateId, container);
		}

S
Sandeep Somavarapu 已提交
184 185 186
		if (templateId === SearchRenderer.FILE_MATCH_TEMPLATE_ID) {
			return this.renderFileMatchTemplate(tree, templateId, container);
		}
187

S
Sandeep Somavarapu 已提交
188 189 190
		if (templateId === SearchRenderer.MATCH_TEMPLATE_ID) {
			return this.renderMatchTemplate(tree, templateId, container);
		}
191

S
Sandeep Somavarapu 已提交
192 193
		return null;
	}
194

S
Sandeep Somavarapu 已提交
195
	public renderElement(tree: ITree, element: any, templateId: string, templateData: any): void {
196 197 198
		if (SearchRenderer.FOLDER_MATCH_TEMPLATE_ID === templateId) {
			this.renderFolderMatch(tree, <FolderMatch>element, <IFolderMatchTemplate>templateData);
		} else if (SearchRenderer.FILE_MATCH_TEMPLATE_ID === templateId) {
S
Sandeep Somavarapu 已提交
199
			this.renderFileMatch(tree, <FileMatch>element, <IFileMatchTemplate>templateData);
S
Sandeep Somavarapu 已提交
200 201
		} else if (SearchRenderer.MATCH_TEMPLATE_ID === templateId) {
			this.renderMatch(tree, <Match>element, <IMatchTemplate>templateData);
202
		}
S
Sandeep Somavarapu 已提交
203
	}
204

205
	private renderFolderMatchTemplate(tree: ITree, templateId: string, container: HTMLElement): IFolderMatchTemplate {
206
		let folderMatchElement = DOM.append(container, DOM.$('.foldermatch'));
207 208 209
		const label = this.instantiationService.createInstance(FileLabel, folderMatchElement, void 0);
		const badge = new CountBadge(DOM.append(folderMatchElement, DOM.$('.badge')));
		this._register(attachBadgeStyler(badge, this.themeService));
210 211
		const actions = new ActionBar(folderMatchElement, { animated: false });
		return { label, badge, actions };
212 213
	}

S
Sandeep Somavarapu 已提交
214 215 216 217
	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);
		const badge = new CountBadge(DOM.append(fileMatchElement, DOM.$('.badge')));
218
		this._register(attachBadgeStyler(badge, this.themeService));
S
Sandeep Somavarapu 已提交
219 220
		const actions = new ActionBar(fileMatchElement, { animated: false });
		return { label, badge, actions };
S
Sandeep Somavarapu 已提交
221 222 223 224 225
	}

	private renderMatchTemplate(tree: ITree, templateId: string, container: HTMLElement): IMatchTemplate {
		DOM.addClass(container, 'linematch');

S
Sandeep Somavarapu 已提交
226
		const parent = DOM.append(container, DOM.$('a.plain.match'));
S
Sandeep Somavarapu 已提交
227 228 229 230
		const before = DOM.append(parent, DOM.$('span'));
		const match = DOM.append(parent, DOM.$('span.findInFileMatch'));
		const replace = DOM.append(parent, DOM.$('span.replaceMatch'));
		const after = DOM.append(parent, DOM.$('span'));
S
Sandeep Somavarapu 已提交
231
		const actions = new ActionBar(container, { animated: false });
S
Sandeep Somavarapu 已提交
232 233 234 235 236 237

		return {
			parent,
			before,
			match,
			replace,
S
Sandeep Somavarapu 已提交
238 239
			after,
			actions
S
Sandeep Somavarapu 已提交
240 241 242
		};
	}

243
	private renderFolderMatch(tree: ITree, folderMatch: FolderMatch, templateData: IFolderMatchTemplate): void {
244
		if (folderMatch.hasRoot()) {
245
			templateData.label.setFile(folderMatch.resource(), { fileKind: FileKind.FOLDER });
246 247 248
		} else {
			templateData.label.setValue(nls.localize('searchFolderMatch.other.label', "Other files"));
		}
249
		let count = folderMatch.fileCount();
250
		templateData.badge.setCount(count);
251
		templateData.badge.setTitleFormat(count > 1 ? nls.localize('searchFileMatches', "{0} files found", count) : nls.localize('searchFileMatch', "{0} file found", count));
252 253

		templateData.actions.clear();
254 255 256 257 258 259 260 261 262

		const input = <SearchResult>tree.getInput();
		const actions: IAction[] = [];
		if (input.searchModel.isReplaceActive() && count > 0) {
			actions.push(this.instantiationService.createInstance(ReplaceAllInFolderAction, tree, folderMatch));
		}

		actions.push(new RemoveAction(tree, folderMatch));
		templateData.actions.push(actions, { icon: true, label: false });
263 264
	}

S
Sandeep Somavarapu 已提交
265
	private renderFileMatch(tree: ITree, fileMatch: FileMatch, templateData: IFileMatchTemplate): void {
266 267 268
		const folderMatch = fileMatch.parent();
		const root = folderMatch.hasRoot() ? folderMatch.resource() : undefined;
		templateData.label.setFile(fileMatch.resource(), { root });
S
Sandeep Somavarapu 已提交
269 270 271
		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));
S
Sandeep Somavarapu 已提交
272 273 274 275 276 277

		let input = <SearchResult>tree.getInput();
		templateData.actions.clear();

		const actions: IAction[] = [];
		if (input.searchModel.isReplaceActive() && count > 0) {
278
			actions.push(this.instantiationService.createInstance(ReplaceAllAction, tree, fileMatch, this.searchView));
S
Sandeep Somavarapu 已提交
279 280 281
		}
		actions.push(new RemoveAction(tree, fileMatch));
		templateData.actions.push(actions, { icon: true, label: false });
S
Sandeep Somavarapu 已提交
282 283 284 285 286 287 288
	}

	private renderMatch(tree: ITree, match: Match, templateData: IMatchTemplate): void {
		let preview = match.preview();
		const searchModel: SearchModel = (<SearchResult>tree.getInput()).searchModel;
		const replace = searchModel.isReplaceActive() && !!searchModel.replaceString;

S
Sandeep Somavarapu 已提交
289 290
		templateData.before.textContent = preview.before;
		templateData.match.textContent = preview.inside;
S
Sandeep Somavarapu 已提交
291
		DOM.toggleClass(templateData.match, 'replace', replace);
S
Sandeep Somavarapu 已提交
292 293
		templateData.replace.textContent = replace ? match.replaceString : '';
		templateData.after.textContent = preview.after;
S
Sandeep Somavarapu 已提交
294
		templateData.parent.title = (preview.before + (replace ? match.replaceString : preview.inside) + preview.after).trim().substr(0, 999);
S
Sandeep Somavarapu 已提交
295 296 297

		templateData.actions.clear();
		if (searchModel.isReplaceActive()) {
298
			templateData.actions.push([this.instantiationService.createInstance(ReplaceAction, tree, match, this.searchView), new RemoveAction(tree, match)], { icon: true, label: false });
S
Sandeep Somavarapu 已提交
299 300
		} else {
			templateData.actions.push([new RemoveAction(tree, match)], { icon: true, label: false });
S
Sandeep Somavarapu 已提交
301
		}
S
Sandeep Somavarapu 已提交
302 303 304
	}

	public disposeTemplate(tree: ITree, templateId: string, templateData: any): void {
305 306 307
		if (SearchRenderer.FOLDER_MATCH_TEMPLATE_ID === templateId) {
			(<IFolderMatchTemplate>templateData).label.dispose();
		}
S
Sandeep Somavarapu 已提交
308 309 310
		if (SearchRenderer.FILE_MATCH_TEMPLATE_ID === templateId) {
			(<IFileMatchTemplate>templateData).label.dispose();
		}
311 312 313 314 315 316 317 318 319
	}
}

export class SearchAccessibilityProvider implements IAccessibilityProvider {

	constructor( @IWorkspaceContextService private contextService: IWorkspaceContextService) {
	}

	public getAriaLabel(tree: ITree, element: FileMatchOrMatch): string {
320 321 322 323
		if (element instanceof FolderMatch) {
			return nls.localize('folderMatchAriaLabel', "{0} matches in folder root {1}, Search result", element.count(), element.name());
		}

324
		if (element instanceof FileMatch) {
325
			const path = getPathLabel(element.resource(), this.contextService) || element.resource().fsPath;
326 327 328 329 330

			return nls.localize('fileMatchAriaLabel', "{0} matches in file {1} of folder {2}, Search result", element.count(), element.name(), paths.dirname(path));
		}

		if (element instanceof Match) {
S
Sandeep Somavarapu 已提交
331 332 333
			const match = <Match>element;
			const searchModel: SearchModel = (<SearchResult>tree.getInput()).searchModel;
			const replace = searchModel.isReplaceActive() && !!searchModel.replaceString;
334
			const matchString = match.getMatchString();
S
Sandeep Somavarapu 已提交
335 336
			const range = match.range();
			if (replace) {
337
				return nls.localize('replacePreviewResultAria', "Replace term {0} with {1} at column position {2} in line with text {3}", matchString, match.replaceString, range.startColumn + 1, match.text());
S
Sandeep Somavarapu 已提交
338
			}
339
			return nls.localize('searchResultAria', "Found term {0} at column position {1} in line with text {2}", matchString, range.startColumn + 1, match.text());
340
		}
M
Matt Bierner 已提交
341
		return undefined;
342 343 344 345 346 347
	}
}

export class SearchFilter implements IFilter {

	public isVisible(tree: ITree, element: any): boolean {
348
		return !(element instanceof FileMatch || element instanceof FolderMatch) || element.matches().length > 0;
349
	}
350
}