searchResultsView.ts 10.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import nls = require('vs/nls');
import strings = require('vs/base/common/strings');
import platform = require('vs/base/common/platform');
import errors = require('vs/base/common/errors');
import paths = require('vs/base/common/paths');
import dom = require('vs/base/browser/dom');
import { $ } from 'vs/base/browser/builder';
import { TPromise } from 'vs/base/common/winjs.base';
import { IAction, IActionRunner } from 'vs/base/common/actions';
import { ActionsRenderer } from 'vs/base/parts/tree/browser/actionsRenderer';
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
17
import { FileLabel } from 'vs/workbench/browser/labels';
18
import { ITree, IElementCallback, IDataSource, ISorter, IAccessibilityProvider, IFilter } from 'vs/base/parts/tree/browser/tree';
J
Johannes Rieken 已提交
19
import { ClickBehavior, DefaultController } from 'vs/base/parts/tree/browser/treeDefaults';
20
import { ContributableActionProvider } from 'vs/workbench/browser/actionBarRegistry';
S
Sandeep Somavarapu 已提交
21
import { Match, SearchResult, FileMatch, FileMatchOrMatch, SearchModel } from 'vs/workbench/parts/search/common/searchModel';
22 23 24
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { Range } from 'vs/editor/common/core/range';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
J
Johannes Rieken 已提交
25
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
26
import { SearchViewlet } from 'vs/workbench/parts/search/browser/searchViewlet';
27 28
import { RemoveAction, ReplaceAllAction, ReplaceAction } from 'vs/workbench/parts/search/browser/searchActions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
29 30 31

export class SearchDataSource implements IDataSource {

32 33
	private static AUTOEXPAND_CHILD_LIMIT = 10;

34 35 36 37 38 39 40 41 42 43 44 45
	public getId(tree: ITree, element: any): string {
		if (element instanceof FileMatch) {
			return element.id();
		}

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

		return 'root';
	}

46
	private _getChildren(element: any): any[] {
47
		if (element instanceof FileMatch) {
48
			return element.matches();
49
		} else if (element instanceof SearchResult) {
50
			return element.matches();
51 52
		}

53 54 55 56 57
		return [];
	}

	public getChildren(tree: ITree, element: any): TPromise<any[]> {
		return TPromise.as(this._getChildren(element));
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
	}

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

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

		if (element instanceof Match) {
			value = element.parent();
		} else if (element instanceof FileMatch) {
			value = element.parent();
		}

		return TPromise.as(value);
	}
75 76 77 78 79

	public shouldAutoexpand(tree: ITree, element: any): boolean {
		const numChildren = this._getChildren(element).length;
		return numChildren > 0 && numChildren < SearchDataSource.AUTOEXPAND_CHILD_LIMIT;
	}
80 81 82 83 84 85
}

export class SearchSorter implements ISorter {

	public compare(tree: ITree, elementA: FileMatchOrMatch, elementB: FileMatchOrMatch): number {
		if (elementA instanceof FileMatch && elementB instanceof FileMatch) {
86
			return elementA.resource().fsPath.localeCompare(elementB.resource().fsPath) || elementA.name().localeCompare(elementB.name());
87 88 89 90 91
		}

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

		return undefined;
94 95 96 97 98
	}
}

class SearchActionProvider extends ContributableActionProvider {

99 100 101 102
	constructor(private viewlet: SearchViewlet, @IInstantiationService private instantiationService: IInstantiationService) {
		super();
	}

103
	public hasActions(tree: ITree, element: any): boolean {
J
Johannes Rieken 已提交
104
		let input = <SearchResult>tree.getInput();
105
		return element instanceof FileMatch || (element instanceof Match && input.searchModel.isReplaceActive()) || super.hasActions(tree, element);
106 107 108 109
	}

	public getActions(tree: ITree, element: any): TPromise<IAction[]> {
		return super.getActions(tree, element).then(actions => {
J
Johannes Rieken 已提交
110
			let input = <SearchResult>tree.getInput();
111 112
			if (element instanceof FileMatch) {
				actions.unshift(new RemoveAction(tree, element));
113
				if (input.searchModel.isReplaceActive() && element.count() > 0) {
114 115 116
					actions.unshift(this.instantiationService.createInstance(ReplaceAllAction, tree, element, this.viewlet));
				}
			}
S
Sandeep Somavarapu 已提交
117
			if (element instanceof Match) {
118
				if (input.searchModel.isReplaceActive()) {
119 120
					actions.unshift(this.instantiationService.createInstance(ReplaceAction, tree, element, this.viewlet), new RemoveAction(tree, element));
				}
121 122 123 124 125 126 127 128 129
			}

			return actions;
		});
	}
}

export class SearchRenderer extends ActionsRenderer {

130
	constructor(actionRunner: IActionRunner, viewlet: SearchViewlet, @IWorkspaceContextService private contextService: IWorkspaceContextService,
J
Johannes Rieken 已提交
131
		@IInstantiationService private instantiationService: IInstantiationService) {
132
		super({
133
			actionProvider: instantiationService.createInstance(SearchActionProvider, viewlet),
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
			actionRunner: actionRunner
		});
	}

	public getContentHeight(tree: ITree, element: any): number {
		return 22;
	}

	public renderContents(tree: ITree, element: FileMatchOrMatch, domElement: HTMLElement, previousCleanupFn: IElementCallback): IElementCallback {

		// File
		if (element instanceof FileMatch) {
			let fileMatch = <FileMatch>element;
			let container = $('.filematch');

149 150
			const label = this.instantiationService.createInstance(FileLabel, container.getHTMLElement(), void 0);
			label.setFile(fileMatch.resource());
151

152
			let len = fileMatch.count();
153

154 155 156
			let badge = $('.badge');
			new CountBadge(badge.getHTMLElement(), len, len > 1 ? nls.localize('searchMatches', "{0} matches found", len) : nls.localize('searchMatch', "{0} match found", len));
			badge.appendTo(container);
157 158 159

			container.appendTo(domElement);

160
			return label.dispose.bind(label);
161 162 163 164 165
		}

		// Match
		else if (element instanceof Match) {
			dom.addClass(domElement, 'linematch');
J
Johannes Rieken 已提交
166
			let match = <Match>element;
167
			let elements: string[] = [];
S
Sandeep Somavarapu 已提交
168
			let preview = match.preview();
169 170 171

			elements.push('<span>');
			elements.push(strings.escape(preview.before));
J
Johannes Rieken 已提交
172
			let searchModel: SearchModel = (<SearchResult>tree.getInput()).searchModel;
173

J
Johannes Rieken 已提交
174
			let showReplaceText = searchModel.isReplaceActive() && !!searchModel.replaceString;
175
			elements.push('</span><span class="' + (showReplaceText ? 'replace ' : '') + 'findInFileMatch">');
S
Sandeep Somavarapu 已提交
176
			elements.push(strings.escape(preview.inside));
177
			if (showReplaceText) {
S
Sandeep Somavarapu 已提交
178
				elements.push('</span><span class="replaceMatch">');
S
Sandeep Somavarapu 已提交
179
				elements.push(strings.escape(match.replaceString));
180
			}
181 182 183 184 185 186
			elements.push('</span><span>');
			elements.push(strings.escape(preview.after));
			elements.push('</span>');

			$('a.plain')
				.innerHtml(elements.join(strings.empty))
S
Sandeep Somavarapu 已提交
187
				.title((preview.before + (showReplaceText ? match.replaceString : preview.inside) + preview.after).trim().substr(0, 999))
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
				.appendTo(domElement);
		}

		return null;
	}
}

export class SearchAccessibilityProvider implements IAccessibilityProvider {

	constructor( @IWorkspaceContextService private contextService: IWorkspaceContextService) {
	}

	public getAriaLabel(tree: ITree, element: FileMatchOrMatch): string {
		if (element instanceof FileMatch) {
			const path = this.contextService.toWorkspaceRelativePath(element.resource()) || element.resource().fsPath;

			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) {
J
Johannes Rieken 已提交
208 209
			let match = <Match>element;
			let input = <SearchResult>tree.getInput();
210
			if (input.searchModel.isReplaceActive()) {
S
Sandeep Somavarapu 已提交
211 212
				let preview = match.preview();
				return nls.localize('replacePreviewResultAria', "Replace preview result, {0}", preview.before + match.replaceString + preview.after);
S
Sandeep Somavarapu 已提交
213
			}
S
Sandeep Somavarapu 已提交
214
			return nls.localize('searchResultAria', "{0}, Search result", match.text());
215
		}
M
Matt Bierner 已提交
216
		return undefined;
217 218 219 220 221
	}
}

export class SearchController extends DefaultController {

222
	constructor(private viewlet: SearchViewlet, @IInstantiationService private instantiationService: IInstantiationService) {
223 224 225 226 227 228 229 230 231 232 233 234
		super({ clickBehavior: ClickBehavior.ON_MOUSE_DOWN, keyboardSupport: false });

		// TODO@Rob these should be commands

		// Up (from results to inputs)
		this.downKeyBindingDispatcher.set(KeyCode.UpArrow, this.onUp.bind(this));

		// Open to side
		this.upKeyBindingDispatcher.set(platform.isMacintosh ? KeyMod.WinCtrl | KeyCode.Enter : KeyMod.CtrlCmd | KeyCode.Enter, this.onEnter.bind(this));

		// Delete
		this.downKeyBindingDispatcher.set(platform.isMacintosh ? KeyMod.CtrlCmd | KeyCode.Backspace : KeyCode.Delete, (tree: ITree, event: any) => { this.onDelete(tree, event); });
235

236 237
		// Cancel search
		this.downKeyBindingDispatcher.set(KeyCode.Escape, (tree: ITree, event: any) => { this.onEscape(tree, event); });
238

239
		// Replace / Replace All
240 241
		this.downKeyBindingDispatcher.set(ReplaceAllAction.KEY_BINDING, (tree: ITree, event: any) => { this.onReplaceAll(tree, event); });
		this.downKeyBindingDispatcher.set(ReplaceAction.KEY_BINDING, (tree: ITree, event: any) => { this.onReplace(tree, event); });
242 243 244 245 246 247 248 249 250 251 252
	}

	protected onEscape(tree: ITree, event: IKeyboardEvent): boolean {
		if (this.viewlet.cancelSearch()) {
			return true;
		}

		return super.onEscape(tree, event);
	}

	private onDelete(tree: ITree, event: IKeyboardEvent): boolean {
J
Johannes Rieken 已提交
253
		let input = <SearchResult>tree.getInput();
254 255
		let result = false;
		let element = tree.getFocus();
256
		if (element instanceof FileMatch ||
J
Johannes Rieken 已提交
257
			(element instanceof Match && input.searchModel.isReplaceActive())) {
258 259 260 261 262 263
			new RemoveAction(tree, element).run().done(null, errors.onUnexpectedError);
			result = true;
		}

		return result;
	}
264

265
	private onReplace(tree: ITree, event: IKeyboardEvent): boolean {
J
Johannes Rieken 已提交
266
		let input = <SearchResult>tree.getInput();
267 268
		let result = false;
		let element = tree.getFocus();
S
Sandeep Somavarapu 已提交
269
		if (element instanceof Match && input.searchModel.isReplaceActive()) {
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
			this.instantiationService.createInstance(ReplaceAction, tree, element, this.viewlet).run().done(null, errors.onUnexpectedError);
			result = true;
		}

		return result;
	}

	private onReplaceAll(tree: ITree, event: IKeyboardEvent): boolean {
		let result = false;
		let element = tree.getFocus();
		if (element instanceof FileMatch && element.count() > 0) {
			this.instantiationService.createInstance(ReplaceAllAction, tree, element, this.viewlet).run().done(null, errors.onUnexpectedError);
			result = true;
		}

		return result;
	}

288 289 290 291 292
	protected onUp(tree: ITree, event: IKeyboardEvent): boolean {
		if (tree.getNavigator().first() === tree.getFocus()) {
			this.viewlet.moveFocusFromResults();
			return true;
		}
R
roblou 已提交
293

294
		return false;
295
	}
296 297 298 299 300 301 302 303
}

export class SearchFilter implements IFilter {

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