searchResultsView.ts 15.2 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
import * as nls from 'vs/nls';
7
import * as resources from 'vs/base/common/resources';
S
Sandeep Somavarapu 已提交
8 9
import * as paths from 'vs/base/common/paths';
import * as DOM from 'vs/base/browser/dom';
10
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
11 12
import { TPromise } from 'vs/base/common/winjs.base';
import { IAction, IActionRunner } from 'vs/base/common/actions';
R
Rob Lourens 已提交
13
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
14
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
15
import { FileLabel } from 'vs/workbench/browser/labels';
R
Rob Lourens 已提交
16 17
import { ITree, IDataSource, IAccessibilityProvider, IFilter, IRenderer, ContextMenuEvent, ISorter } from 'vs/base/parts/tree/browser/tree';
import { Match, SearchResult, FileMatch, FileMatchOrMatch, SearchModel, FolderMatch, searchMatchComparer, RenderableMatch } from 'vs/workbench/parts/search/common/searchModel';
18
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
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 { FileKind } from 'vs/platform/files/common/files';
R
Rob Lourens 已提交
25 26
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
R
Rob Lourens 已提交
27
import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions';
R
Rob Lourens 已提交
28
import { WorkbenchTreeController, WorkbenchTree } from 'vs/platform/list/browser/listService';
29
import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem';
I
isidor 已提交
30
import { IUriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay';
31 32 33

export class SearchDataSource implements IDataSource {

34
	private static readonly AUTOEXPAND_CHILD_LIMIT = 10;
35

36 37 38
	private includeFolderMatch: boolean;
	private listener: IDisposable;

M
Matt Bierner 已提交
39
	constructor(@IWorkspaceContextService private contextService: IWorkspaceContextService) {
40 41 42 43 44 45 46
		this.updateIncludeFolderMatch();
		this.listener = this.contextService.onDidChangeWorkbenchState(() => this.updateIncludeFolderMatch());
	}

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

48
	public getId(tree: ITree, element: any): string {
49 50 51 52
		if (element instanceof FolderMatch) {
			return element.id();
		}

53 54 55 56 57 58 59 60 61 62 63
		if (element instanceof FileMatch) {
			return element.id();
		}

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

		return 'root';
	}

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

76 77 78 79 80
		return [];
	}

	public getChildren(tree: ITree, element: any): TPromise<any[]> {
		return TPromise.as(this._getChildren(element));
81 82 83
	}

	public hasChildren(tree: ITree, element: any): boolean {
84
		return element instanceof FileMatch || element instanceof FolderMatch || element instanceof SearchResult;
85 86 87 88 89 90 91 92
	}

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

		if (element instanceof Match) {
			value = element.parent();
		} else if (element instanceof FileMatch) {
93
			value = this.includeFolderMatch ? element.parent() : element.parent().parent();
94 95
		} else if (element instanceof FolderMatch) {
			value = element.parent();
96 97 98 99
		}

		return TPromise.as(value);
	}
100 101 102

	public shouldAutoexpand(tree: ITree, element: any): boolean {
		const numChildren = this._getChildren(element).length;
103 104 105 106
		if (numChildren <= 0) {
			return false;
		}
		return numChildren < SearchDataSource.AUTOEXPAND_CHILD_LIMIT || element instanceof FolderMatch;
107
	}
108 109 110 111

	public dispose(): void {
		this.listener = dispose(this.listener);
	}
112 113
}

R
Rob Lourens 已提交
114 115 116 117 118
export class SearchSorter implements ISorter {
	public compare(tree: ITree, elementA: RenderableMatch, elementB: RenderableMatch): number {
		return searchMatchComparer(elementA, elementB);
	}
}
119

120 121 122
interface IFolderMatchTemplate {
	label: FileLabel;
	badge: CountBadge;
123
	actions: ActionBar;
124 125
}

S
Sandeep Somavarapu 已提交
126
interface IFileMatchTemplate {
127
	el: HTMLElement;
S
Sandeep Somavarapu 已提交
128 129
	label: FileLabel;
	badge: CountBadge;
S
Sandeep Somavarapu 已提交
130
	actions: ActionBar;
S
Sandeep Somavarapu 已提交
131 132 133 134 135 136
}

interface IMatchTemplate {
	parent: HTMLElement;
	before: HTMLElement;
	match: HTMLElement;
S
Sandeep Somavarapu 已提交
137
	replace: HTMLElement;
S
Sandeep Somavarapu 已提交
138
	after: HTMLElement;
S
Sandeep Somavarapu 已提交
139
	actions: ActionBar;
S
Sandeep Somavarapu 已提交
140 141 142 143
}

export class SearchRenderer extends Disposable implements IRenderer {

144 145 146
	private static readonly FOLDER_MATCH_TEMPLATE_ID = 'folderMatch';
	private static readonly FILE_MATCH_TEMPLATE_ID = 'fileMatch';
	private static readonly MATCH_TEMPLATE_ID = 'match';
147

148 149
	constructor(
		actionRunner: IActionRunner,
150
		private searchView: SearchView,
151
		@IInstantiationService private instantiationService: IInstantiationService,
152 153
		@IThemeService private themeService: IThemeService,
		@IWorkspaceContextService protected contextService: IWorkspaceContextService
154
	) {
S
Sandeep Somavarapu 已提交
155
		super();
156 157
	}

S
Sandeep Somavarapu 已提交
158
	public getHeight(tree: ITree, element: any): number {
159 160 161
		return 22;
	}

S
Sandeep Somavarapu 已提交
162
	public getTemplateId(tree: ITree, element: any): string {
163 164 165
		if (element instanceof FolderMatch) {
			return SearchRenderer.FOLDER_MATCH_TEMPLATE_ID;
		} else if (element instanceof FileMatch) {
S
Sandeep Somavarapu 已提交
166 167 168 169 170 171
			return SearchRenderer.FILE_MATCH_TEMPLATE_ID;
		} else if (element instanceof Match) {
			return SearchRenderer.MATCH_TEMPLATE_ID;
		}
		return null;
	}
172

S
Sandeep Somavarapu 已提交
173
	public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any {
174 175 176 177
		if (templateId === SearchRenderer.FOLDER_MATCH_TEMPLATE_ID) {
			return this.renderFolderMatchTemplate(tree, templateId, container);
		}

S
Sandeep Somavarapu 已提交
178 179 180
		if (templateId === SearchRenderer.FILE_MATCH_TEMPLATE_ID) {
			return this.renderFileMatchTemplate(tree, templateId, container);
		}
181

S
Sandeep Somavarapu 已提交
182 183 184
		if (templateId === SearchRenderer.MATCH_TEMPLATE_ID) {
			return this.renderMatchTemplate(tree, templateId, container);
		}
185

S
Sandeep Somavarapu 已提交
186 187
		return null;
	}
188

S
Sandeep Somavarapu 已提交
189
	public renderElement(tree: ITree, element: any, templateId: string, templateData: any): void {
190 191 192
		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 已提交
193
			this.renderFileMatch(tree, <FileMatch>element, <IFileMatchTemplate>templateData);
S
Sandeep Somavarapu 已提交
194 195
		} else if (SearchRenderer.MATCH_TEMPLATE_ID === templateId) {
			this.renderMatch(tree, <Match>element, <IMatchTemplate>templateData);
196
		}
S
Sandeep Somavarapu 已提交
197
	}
198

199
	private renderFolderMatchTemplate(tree: ITree, templateId: string, container: HTMLElement): IFolderMatchTemplate {
200
		let folderMatchElement = DOM.append(container, DOM.$('.foldermatch'));
201 202 203
		const label = this.instantiationService.createInstance(FileLabel, folderMatchElement, void 0);
		const badge = new CountBadge(DOM.append(folderMatchElement, DOM.$('.badge')));
		this._register(attachBadgeStyler(badge, this.themeService));
204 205
		const actions = new ActionBar(folderMatchElement, { animated: false });
		return { label, badge, actions };
206 207
	}

S
Sandeep Somavarapu 已提交
208 209 210 211
	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')));
212
		this._register(attachBadgeStyler(badge, this.themeService));
S
Sandeep Somavarapu 已提交
213
		const actions = new ActionBar(fileMatchElement, { animated: false });
214
		return { el: fileMatchElement, label, badge, actions };
S
Sandeep Somavarapu 已提交
215 216 217 218 219
	}

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

S
Sandeep Somavarapu 已提交
220
		const parent = DOM.append(container, DOM.$('a.plain.match'));
S
Sandeep Somavarapu 已提交
221 222 223 224
		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 已提交
225
		const actions = new ActionBar(container, { animated: false });
S
Sandeep Somavarapu 已提交
226 227 228 229 230 231

		return {
			parent,
			before,
			match,
			replace,
S
Sandeep Somavarapu 已提交
232 233
			after,
			actions
S
Sandeep Somavarapu 已提交
234 235 236
		};
	}

237
	private renderFolderMatch(tree: ITree, folderMatch: FolderMatch, templateData: IFolderMatchTemplate): void {
238
		if (folderMatch.hasRoot()) {
R
Rob Lourens 已提交
239
			const workspaceFolder = this.contextService.getWorkspaceFolder(folderMatch.resource());
240 241 242 243 244
			if (workspaceFolder && resources.isEqual(workspaceFolder.uri, folderMatch.resource())) {
				templateData.label.setFile(folderMatch.resource(), { fileKind: FileKind.ROOT_FOLDER, hidePath: true });
			} else {
				templateData.label.setFile(folderMatch.resource(), { fileKind: FileKind.FOLDER });
			}
245 246 247
		} else {
			templateData.label.setValue(nls.localize('searchFolderMatch.other.label', "Other files"));
		}
248
		let count = folderMatch.fileCount();
249
		templateData.badge.setCount(count);
250
		templateData.badge.setTitleFormat(count > 1 ? nls.localize('searchFileMatches', "{0} files found", count) : nls.localize('searchFileMatch', "{0} file found", count));
251 252

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

		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 });
262 263
	}

S
Sandeep Somavarapu 已提交
264
	private renderFileMatch(tree: ITree, fileMatch: FileMatch, templateData: IFileMatchTemplate): void {
265
		templateData.el.setAttribute('data-resource', fileMatch.resource().toString());
I
isidor 已提交
266
		templateData.label.setFile(fileMatch.resource());
S
Sandeep Somavarapu 已提交
267 268 269
		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 已提交
270 271 272 273 274 275

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

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

	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 已提交
287 288
		templateData.before.textContent = preview.before;
		templateData.match.textContent = preview.inside;
S
Sandeep Somavarapu 已提交
289
		DOM.toggleClass(templateData.match, 'replace', replace);
S
Sandeep Somavarapu 已提交
290 291
		templateData.replace.textContent = replace ? match.replaceString : '';
		templateData.after.textContent = preview.after;
S
Sandeep Somavarapu 已提交
292
		templateData.parent.title = (preview.before + (replace ? match.replaceString : preview.inside) + preview.after).trim().substr(0, 999);
S
Sandeep Somavarapu 已提交
293 294 295

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

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

export class SearchAccessibilityProvider implements IAccessibilityProvider {

320
	constructor(
I
isidor 已提交
321
		@IUriDisplayService private uriDisplayService: IUriDisplayService
322
	) {
323 324 325
	}

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

330
		if (element instanceof FileMatch) {
I
isidor 已提交
331
			const path = this.uriDisplayService.getLabel(element.resource(), true) || element.resource().fsPath;
332 333 334 335 336

			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 已提交
337 338 339
			const match = <Match>element;
			const searchModel: SearchModel = (<SearchResult>tree.getInput()).searchModel;
			const replace = searchModel.isReplaceActive() && !!searchModel.replaceString;
340
			const matchString = match.getMatchString();
S
Sandeep Somavarapu 已提交
341
			const range = match.range();
R
Rob Lourens 已提交
342
			const matchText = match.text().substr(0, range.endColumn + 150);
S
Sandeep Somavarapu 已提交
343
			if (replace) {
R
Rob Lourens 已提交
344
				return nls.localize('replacePreviewResultAria', "Replace term {0} with {1} at column position {2} in line with text {3}", matchString, match.replaceString, range.startColumn + 1, matchText);
S
Sandeep Somavarapu 已提交
345
			}
R
Rob Lourens 已提交
346 347

			return nls.localize('searchResultAria', "Found term {0} at column position {1} in line with text {2}", matchString, range.startColumn + 1, matchText);
348
		}
M
Matt Bierner 已提交
349
		return undefined;
350 351 352 353 354 355
	}
}

export class SearchFilter implements IFilter {

	public isVisible(tree: ITree, element: any): boolean {
356
		return !(element instanceof FileMatch || element instanceof FolderMatch) || element.matches().length > 0;
357
	}
358
}
R
Rob Lourens 已提交
359 360

export class SearchTreeController extends WorkbenchTreeController {
R
Rob Lourens 已提交
361 362
	private contextMenu: IMenu;

R
Rob Lourens 已提交
363 364 365 366 367 368 369 370 371
	constructor(
		@IContextMenuService private contextMenuService: IContextMenuService,
		@IMenuService private menuService: IMenuService,
		@IConfigurationService configurationService: IConfigurationService
	) {
		super({}, configurationService);
	}

	public onContextMenu(tree: WorkbenchTree, element: any, event: ContextMenuEvent): boolean {
R
Rob Lourens 已提交
372 373
		if (!this.contextMenu) {
			this.contextMenu = this.menuService.createMenu(MenuId.SearchContext, tree.contextKeyService);
R
Rob Lourens 已提交
374 375
		}

376
		tree.setFocus(element, { preventOpenOnFocus: true });
R
Rob Lourens 已提交
377

R
Rob Lourens 已提交
378 379 380 381 382
		const anchor = { x: event.posx, y: event.posy };
		this.contextMenuService.showContextMenu({
			getAnchor: () => anchor,

			getActions: () => {
R
Rob Lourens 已提交
383
				const actions: IAction[] = [];
384
				fillInContextMenuActions(this.contextMenu, { shouldForwardArgs: true }, actions, this.contextMenuService);
R
Rob Lourens 已提交
385
				return TPromise.as(actions);
386 387 388
			},

			getActionsContext: () => element
R
Rob Lourens 已提交
389 390 391 392
		});

		return true;
	}
393
}