searchResultsView.ts 17.0 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 DOM from 'vs/base/browser/dom';
R
Rob Lourens 已提交
7
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
8
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
9 10 11 12 13 14 15 16
import { IAction } from 'vs/base/common/actions';
import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
import * as paths from 'vs/base/common/paths';
import * as resources from 'vs/base/common/resources';
import { ContextMenuEvent, IAccessibilityProvider, IDataSource, IFilter, IRenderer, ISorter, ITree } from 'vs/base/parts/tree/browser/tree';
import * as nls from 'vs/nls';
import { fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem';
import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions';
R
Rob Lourens 已提交
17 18
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
19 20
import { FileKind } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
I
isidor 已提交
21
import { ILabelService } from 'vs/platform/label/common/label';
22 23 24 25 26 27 28 29 30
import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService';
import { ISearchConfigurationProperties } from 'vs/platform/search/common/search';
import { attachBadgeStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { FileLabel } from 'vs/workbench/browser/labels';
import { RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction } from 'vs/workbench/parts/search/browser/searchActions';
import { SearchView } from 'vs/workbench/parts/search/browser/searchView';
import { FileMatch, FileMatchOrMatch, FolderMatch, Match, RenderableMatch, searchMatchComparer, SearchModel, SearchResult } from 'vs/workbench/parts/search/common/searchModel';
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;

N
Nil 已提交
39 40 41 42
	constructor(
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
		@IConfigurationService private configurationService: IConfigurationService,
	) {
43 44 45 46 47 48 49
		this.updateIncludeFolderMatch();
		this.listener = this.contextService.onDidChangeWorkbenchState(() => this.updateIncludeFolderMatch());
	}

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

51
	public getId(tree: ITree, element: any): string {
52 53 54 55
		if (element instanceof FolderMatch) {
			return element.id();
		}

56 57 58 59 60 61 62 63 64 65 66
		if (element instanceof FileMatch) {
			return element.id();
		}

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

		return 'root';
	}

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

79 80 81
		return [];
	}

82 83
	public getChildren(tree: ITree, element: any): Thenable<any[]> {
		return Promise.resolve(this._getChildren(element));
84 85 86
	}

	public hasChildren(tree: ITree, element: any): boolean {
87
		return element instanceof FileMatch || element instanceof FolderMatch || element instanceof SearchResult;
88 89
	}

90
	public getParent(tree: ITree, element: any): Thenable<any> {
91 92 93 94 95
		let value: any = null;

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

101
		return Promise.resolve(value);
102
	}
103 104 105

	public shouldAutoexpand(tree: ITree, element: any): boolean {
		const numChildren = this._getChildren(element).length;
106 107 108
		if (numChildren <= 0) {
			return false;
		}
N
Nil 已提交
109

R
Rob Lourens 已提交
110
		const collapseOption = this.configurationService.getValue('search.collapseResults');
N
Nil 已提交
111 112
		if (collapseOption === 'alwaysCollapse') {
			return false;
R
Rob Lourens 已提交
113 114
		} else if (collapseOption === 'alwaysExpand') {
			return true;
N
Nil 已提交
115 116
		}

117
		return numChildren < SearchDataSource.AUTOEXPAND_CHILD_LIMIT || element instanceof FolderMatch;
118
	}
119 120 121 122

	public dispose(): void {
		this.listener = dispose(this.listener);
	}
123 124
}

R
Rob Lourens 已提交
125 126 127 128 129
export class SearchSorter implements ISorter {
	public compare(tree: ITree, elementA: RenderableMatch, elementB: RenderableMatch): number {
		return searchMatchComparer(elementA, elementB);
	}
}
130

131 132 133
interface IFolderMatchTemplate {
	label: FileLabel;
	badge: CountBadge;
134
	actions: ActionBar;
135 136
}

S
Sandeep Somavarapu 已提交
137
interface IFileMatchTemplate {
138
	el: HTMLElement;
S
Sandeep Somavarapu 已提交
139 140
	label: FileLabel;
	badge: CountBadge;
S
Sandeep Somavarapu 已提交
141
	actions: ActionBar;
S
Sandeep Somavarapu 已提交
142 143 144 145 146 147
}

interface IMatchTemplate {
	parent: HTMLElement;
	before: HTMLElement;
	match: HTMLElement;
S
Sandeep Somavarapu 已提交
148
	replace: HTMLElement;
S
Sandeep Somavarapu 已提交
149
	after: HTMLElement;
150
	lineNumber: HTMLElement;
S
Sandeep Somavarapu 已提交
151
	actions: ActionBar;
S
Sandeep Somavarapu 已提交
152 153 154 155
}

export class SearchRenderer extends Disposable implements IRenderer {

156 157 158
	private static readonly FOLDER_MATCH_TEMPLATE_ID = 'folderMatch';
	private static readonly FILE_MATCH_TEMPLATE_ID = 'fileMatch';
	private static readonly MATCH_TEMPLATE_ID = 'match';
159

160
	constructor(
161
		private searchView: SearchView,
162
		@IInstantiationService private instantiationService: IInstantiationService,
163
		@IThemeService private themeService: IThemeService,
164
		@IConfigurationService private configurationService: IConfigurationService,
165
		@IWorkspaceContextService protected contextService: IWorkspaceContextService
166
	) {
S
Sandeep Somavarapu 已提交
167
		super();
168 169
	}

S
Sandeep Somavarapu 已提交
170
	public getHeight(tree: ITree, element: any): number {
171 172 173
		return 22;
	}

S
Sandeep Somavarapu 已提交
174
	public getTemplateId(tree: ITree, element: any): string {
175 176 177
		if (element instanceof FolderMatch) {
			return SearchRenderer.FOLDER_MATCH_TEMPLATE_ID;
		} else if (element instanceof FileMatch) {
S
Sandeep Somavarapu 已提交
178 179 180 181 182 183
			return SearchRenderer.FILE_MATCH_TEMPLATE_ID;
		} else if (element instanceof Match) {
			return SearchRenderer.MATCH_TEMPLATE_ID;
		}
		return null;
	}
184

S
Sandeep Somavarapu 已提交
185
	public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any {
186 187 188 189
		if (templateId === SearchRenderer.FOLDER_MATCH_TEMPLATE_ID) {
			return this.renderFolderMatchTemplate(tree, templateId, container);
		}

S
Sandeep Somavarapu 已提交
190 191 192
		if (templateId === SearchRenderer.FILE_MATCH_TEMPLATE_ID) {
			return this.renderFileMatchTemplate(tree, templateId, container);
		}
193

S
Sandeep Somavarapu 已提交
194 195 196
		if (templateId === SearchRenderer.MATCH_TEMPLATE_ID) {
			return this.renderMatchTemplate(tree, templateId, container);
		}
197

S
Sandeep Somavarapu 已提交
198 199
		return null;
	}
200

S
Sandeep Somavarapu 已提交
201
	public renderElement(tree: ITree, element: any, templateId: string, templateData: any): void {
202 203 204
		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 已提交
205
			this.renderFileMatch(tree, <FileMatch>element, <IFileMatchTemplate>templateData);
S
Sandeep Somavarapu 已提交
206 207
		} else if (SearchRenderer.MATCH_TEMPLATE_ID === templateId) {
			this.renderMatch(tree, <Match>element, <IMatchTemplate>templateData);
208
		}
S
Sandeep Somavarapu 已提交
209
	}
210

211
	private renderFolderMatchTemplate(tree: ITree, templateId: string, container: HTMLElement): IFolderMatchTemplate {
212
		let folderMatchElement = DOM.append(container, DOM.$('.foldermatch'));
213 214 215
		const label = this.instantiationService.createInstance(FileLabel, folderMatchElement, void 0);
		const badge = new CountBadge(DOM.append(folderMatchElement, DOM.$('.badge')));
		this._register(attachBadgeStyler(badge, this.themeService));
216 217
		const actions = new ActionBar(folderMatchElement, { animated: false });
		return { label, badge, actions };
218 219
	}

S
Sandeep Somavarapu 已提交
220 221 222 223
	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')));
224
		this._register(attachBadgeStyler(badge, this.themeService));
S
Sandeep Somavarapu 已提交
225
		const actions = new ActionBar(fileMatchElement, { animated: false });
226
		return { el: fileMatchElement, label, badge, actions };
S
Sandeep Somavarapu 已提交
227 228 229 230 231
	}

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

S
Sandeep Somavarapu 已提交
232
		const parent = DOM.append(container, DOM.$('a.plain.match'));
S
Sandeep Somavarapu 已提交
233 234 235 236
		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'));
237
		const lineNumber = DOM.append(container, DOM.$('span.matchLineNum'));
J
Joao Moreno 已提交
238 239
		const actionBarContainer = DOM.append(container, DOM.$('span.actionBarContainer'));
		const actions = new ActionBar(actionBarContainer, { animated: false });
S
Sandeep Somavarapu 已提交
240 241 242 243 244 245

		return {
			parent,
			before,
			match,
			replace,
S
Sandeep Somavarapu 已提交
246
			after,
247
			lineNumber,
S
Sandeep Somavarapu 已提交
248
			actions
S
Sandeep Somavarapu 已提交
249 250 251
		};
	}

252
	private renderFolderMatch(tree: ITree, folderMatch: FolderMatch, templateData: IFolderMatchTemplate): void {
253
		if (folderMatch.hasResource()) {
R
Rob Lourens 已提交
254
			const workspaceFolder = this.contextService.getWorkspaceFolder(folderMatch.resource());
255 256 257 258 259
			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 });
			}
260 261 262
		} else {
			templateData.label.setValue(nls.localize('searchFolderMatch.other.label', "Other files"));
		}
263
		let count = folderMatch.fileCount();
264
		templateData.badge.setCount(count);
265
		templateData.badge.setTitleFormat(count > 1 ? nls.localize('searchFileMatches', "{0} files found", count) : nls.localize('searchFileMatch', "{0} file found", count));
266 267

		templateData.actions.clear();
268 269 270 271 272 273 274 275 276

		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 });
277 278
	}

S
Sandeep Somavarapu 已提交
279
	private renderFileMatch(tree: ITree, fileMatch: FileMatch, templateData: IFileMatchTemplate): void {
280
		templateData.el.setAttribute('data-resource', fileMatch.resource().toString());
R
Rob Lourens 已提交
281
		templateData.label.setFile(fileMatch.resource(), { hideIcon: false });
S
Sandeep Somavarapu 已提交
282 283 284
		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 已提交
285 286 287 288 289 290

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

		const actions: IAction[] = [];
		if (input.searchModel.isReplaceActive() && count > 0) {
291
			actions.push(this.instantiationService.createInstance(ReplaceAllAction, tree, fileMatch, this.searchView));
S
Sandeep Somavarapu 已提交
292 293 294
		}
		actions.push(new RemoveAction(tree, fileMatch));
		templateData.actions.push(actions, { icon: true, label: false });
S
Sandeep Somavarapu 已提交
295 296 297 298 299 300 301
	}

	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 已提交
302 303
		templateData.before.textContent = preview.before;
		templateData.match.textContent = preview.inside;
S
Sandeep Somavarapu 已提交
304
		DOM.toggleClass(templateData.match, 'replace', replace);
S
Sandeep Somavarapu 已提交
305 306
		templateData.replace.textContent = replace ? match.replaceString : '';
		templateData.after.textContent = preview.after;
S
Sandeep Somavarapu 已提交
307
		templateData.parent.title = (preview.before + (replace ? match.replaceString : preview.inside) + preview.after).trim().substr(0, 999);
S
Sandeep Somavarapu 已提交
308

309 310 311 312 313 314 315 316 317 318
		const numLines = match.range().endLineNumber - match.range().startLineNumber;
		const extraLinesStr = numLines > 0 ? `+${numLines}` : '';

		const showLineNumbers = this.configurationService.getValue<ISearchConfigurationProperties>('search').showLineNumbers;
		const lineNumberStr = showLineNumbers ? `:${match.range().startLineNumber}` : '';
		DOM.toggleClass(templateData.lineNumber, 'show', (numLines > 0) || showLineNumbers);

		templateData.lineNumber.textContent = lineNumberStr + extraLinesStr;
		templateData.lineNumber.setAttribute('title', this.getMatchTitle(match, showLineNumbers));

S
Sandeep Somavarapu 已提交
319 320
		templateData.actions.clear();
		if (searchModel.isReplaceActive()) {
321
			templateData.actions.push([this.instantiationService.createInstance(ReplaceAction, tree, match, this.searchView), new RemoveAction(tree, match)], { icon: true, label: false });
S
Sandeep Somavarapu 已提交
322 323
		} else {
			templateData.actions.push([new RemoveAction(tree, match)], { icon: true, label: false });
S
Sandeep Somavarapu 已提交
324
		}
S
Sandeep Somavarapu 已提交
325 326
	}

327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
	private getMatchTitle(match: Match, showLineNumbers: boolean): string {
		const startLine = match.range().startLineNumber;
		const numLines = match.range().endLineNumber - match.range().startLineNumber;

		const lineNumStr = showLineNumbers ?
			nls.localize('lineNumStr', "From line {0}", startLine, numLines) + ' ' :
			'';

		const numLinesStr = numLines > 0 ?
			'+ ' + nls.localize('numLinesStr', "{0} more lines", numLines) :
			'';

		return lineNumStr + numLinesStr;
	}

S
Sandeep Somavarapu 已提交
342
	public disposeTemplate(tree: ITree, templateId: string, templateData: any): void {
343
		if (SearchRenderer.FOLDER_MATCH_TEMPLATE_ID === templateId) {
B
Benjamin Pasero 已提交
344 345 346 347 348 349 350 351 352 353
			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 已提交
354
		}
355 356 357 358 359
	}
}

export class SearchAccessibilityProvider implements IAccessibilityProvider {

360
	constructor(
I
isidor 已提交
361
		@ILabelService private labelService: ILabelService
362
	) {
363 364 365
	}

	public getAriaLabel(tree: ITree, element: FileMatchOrMatch): string {
366
		if (element instanceof FolderMatch) {
367 368 369
			return element.hasResource() ?
				nls.localize('folderMatchAriaLabel', "{0} matches in folder root {1}, Search result", element.count(), element.name()) :
				nls.localize('otherFilesAriaLabel', "{0} matches outside of the workspace, Search result", element.count());
370 371
		}

372
		if (element instanceof FileMatch) {
373
			const path = this.labelService.getUriLabel(element.resource(), { relative: true }) || element.resource().fsPath;
374 375 376 377 378

			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 已提交
379 380 381
			const match = <Match>element;
			const searchModel: SearchModel = (<SearchResult>tree.getInput()).searchModel;
			const replace = searchModel.isReplaceActive() && !!searchModel.replaceString;
382
			const matchString = match.getMatchString();
S
Sandeep Somavarapu 已提交
383
			const range = match.range();
R
Rob Lourens 已提交
384
			const matchText = match.text().substr(0, range.endColumn + 150);
S
Sandeep Somavarapu 已提交
385
			if (replace) {
R
Rob Lourens 已提交
386
				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 已提交
387
			}
R
Rob Lourens 已提交
388 389

			return nls.localize('searchResultAria', "Found term {0} at column position {1} in line with text {2}", matchString, range.startColumn + 1, matchText);
390
		}
M
Matt Bierner 已提交
391
		return undefined;
392 393 394 395 396 397
	}
}

export class SearchFilter implements IFilter {

	public isVisible(tree: ITree, element: any): boolean {
398
		return !(element instanceof FileMatch || element instanceof FolderMatch) || element.matches().length > 0;
399
	}
400
}
R
Rob Lourens 已提交
401 402

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

R
Rob Lourens 已提交
405 406 407 408 409 410 411 412 413
	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 已提交
414 415
		if (!this.contextMenu) {
			this.contextMenu = this.menuService.createMenu(MenuId.SearchContext, tree.contextKeyService);
R
Rob Lourens 已提交
416
			this.disposables.push(this.contextMenu);
R
Rob Lourens 已提交
417 418
		}

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

R
Rob Lourens 已提交
421 422 423 424 425
		const anchor = { x: event.posx, y: event.posy };
		this.contextMenuService.showContextMenu({
			getAnchor: () => anchor,

			getActions: () => {
R
Rob Lourens 已提交
426
				const actions: IAction[] = [];
427
				fillInContextMenuActions(this.contextMenu, { shouldForwardArgs: true }, actions, this.contextMenuService);
428
				return actions;
429 430 431
			},

			getActionsContext: () => element
R
Rob Lourens 已提交
432 433 434 435
		});

		return true;
	}
436
}