searchResultsView.ts 12.1 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 19
import { LeftRightWidget, IRenderer } from 'vs/base/browser/ui/leftRightWidget/leftRightWidget';
import { ITree, IElementCallback, IDataSource, ISorter, IAccessibilityProvider, IFilter } from 'vs/base/parts/tree/browser/tree';
J
Johannes Rieken 已提交
20
import { ClickBehavior, DefaultController } from 'vs/base/parts/tree/browser/treeDefaults';
21
import { ContributableActionProvider } from 'vs/workbench/browser/actionBarRegistry';
S
Sandeep Somavarapu 已提交
22
import { Match, SearchResult, FileMatch, FileMatchOrMatch, SearchModel } from 'vs/workbench/parts/search/common/searchModel';
23 24 25
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 已提交
26
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
27
import { SearchViewlet } from 'vs/workbench/parts/search/browser/searchViewlet';
28 29
import { RemoveAction, ReplaceAllAction, ReplaceAction } from 'vs/workbench/parts/search/browser/searchActions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77

export class SearchDataSource implements IDataSource {

	public getId(tree: ITree, element: any): string {
		if (element instanceof FileMatch) {
			return element.id();
		}

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

		return 'root';
	}

	public getChildren(tree: ITree, element: any): TPromise<any[]> {
		let value: any[] = [];

		if (element instanceof FileMatch) {
			value = element.matches();
		} else if (element instanceof SearchResult) {
			value = element.matches();
		}

		return TPromise.as(value);
	}

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

export class SearchSorter implements ISorter {

	public compare(tree: ITree, elementA: FileMatchOrMatch, elementB: FileMatchOrMatch): number {
		if (elementA instanceof FileMatch && elementB instanceof FileMatch) {
78
			return elementA.resource().fsPath.localeCompare(elementB.resource().fsPath) || elementA.name().localeCompare(elementB.name());
79 80 81 82 83 84 85 86 87 88
		}

		if (elementA instanceof Match && elementB instanceof Match) {
			return Range.compareRangesUsingStarts(elementA.range(), elementB.range());
		}
	}
}

class SearchActionProvider extends ContributableActionProvider {

89 90 91 92
	constructor(private viewlet: SearchViewlet, @IInstantiationService private instantiationService: IInstantiationService) {
		super();
	}

93
	public hasActions(tree: ITree, element: any): boolean {
J
Johannes Rieken 已提交
94
		let input = <SearchResult>tree.getInput();
95
		return element instanceof FileMatch || (input.searchModel.isReplaceActive() || element instanceof Match) || super.hasActions(tree, element);
96 97 98 99
	}

	public getActions(tree: ITree, element: any): TPromise<IAction[]> {
		return super.getActions(tree, element).then(actions => {
J
Johannes Rieken 已提交
100
			let input = <SearchResult>tree.getInput();
101 102
			if (element instanceof FileMatch) {
				actions.unshift(new RemoveAction(tree, element));
103
				if (input.searchModel.isReplaceActive() && element.count() > 0) {
104 105 106
					actions.unshift(this.instantiationService.createInstance(ReplaceAllAction, tree, element, this.viewlet));
				}
			}
S
Sandeep Somavarapu 已提交
107
			if (element instanceof Match) {
108
				if (input.searchModel.isReplaceActive()) {
109 110
					actions.unshift(this.instantiationService.createInstance(ReplaceAction, tree, element, this.viewlet), new RemoveAction(tree, element));
				}
111 112 113 114 115 116 117 118 119
			}

			return actions;
		});
	}
}

export class SearchRenderer extends ActionsRenderer {

120
	constructor(actionRunner: IActionRunner, viewlet: SearchViewlet, @IWorkspaceContextService private contextService: IWorkspaceContextService,
J
Johannes Rieken 已提交
121
		@IInstantiationService private instantiationService: IInstantiationService) {
122
		super({
123
			actionProvider: instantiationService.createInstance(SearchActionProvider, viewlet),
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
			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');
			let leftRenderer: IRenderer;
			let rightRenderer: IRenderer;
			let widget: LeftRightWidget;

			leftRenderer = (left: HTMLElement): any => {
B
Benjamin Pasero 已提交
143
				const label = this.instantiationService.createInstance(FileLabel, left, void 0);
144
				label.setFile(fileMatch.resource());
145

146
				return () => label.dispose();
147 148 149 150 151
			};

			rightRenderer = (right: HTMLElement) => {
				let len = fileMatch.count();

J
Joao Moreno 已提交
152 153
				new CountBadge(right, len, len > 1 ? nls.localize('searchMatches', "{0} matches found", len) : nls.localize('searchMatch', "{0} match found", len));
				return null;
154 155 156 157 158 159 160 161 162 163 164 165
			};

			widget = new LeftRightWidget(container, leftRenderer, rightRenderer);

			container.appendTo(domElement);

			return widget.dispose.bind(widget);
		}

		// 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 216 217 218 219 220
		}
	}
}

export class SearchController extends DefaultController {

221 222
	private _arrowKeyThrottler = new DumbThrottler(500);

223
	constructor(private viewlet: SearchViewlet, @IInstantiationService private instantiationService: IInstantiationService) {
224 225 226
		super({ clickBehavior: ClickBehavior.ON_MOUSE_DOWN });

		if (platform.isMacintosh) {
A
Alexandru Dima 已提交
227 228
			this.downKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.Backspace, (tree: ITree, event: any) => { this.onDelete(tree, event); });
			this.upKeyBindingDispatcher.set(KeyMod.WinCtrl | KeyCode.Enter, this.onEnter.bind(this));
229
		} else {
A
Alexandru Dima 已提交
230 231
			this.downKeyBindingDispatcher.set(KeyCode.Delete, (tree: ITree, event: any) => { this.onDelete(tree, event); });
			this.upKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.Enter, this.onEnter.bind(this));
232 233
		}

234 235
		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); });
A
Alexandru Dima 已提交
236
		this.downKeyBindingDispatcher.set(KeyCode.Escape, (tree: ITree, event: any) => { this.onEscape(tree, event); });
237 238 239 240 241 242 243 244 245 246 247
	}

	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 已提交
248
		let input = <SearchResult>tree.getInput();
249 250
		let result = false;
		let element = tree.getFocus();
251
		if (element instanceof FileMatch ||
J
Johannes Rieken 已提交
252
			(element instanceof Match && input.searchModel.isReplaceActive())) {
253 254 255 256 257 258
			new RemoveAction(tree, element).run().done(null, errors.onUnexpectedError);
			result = true;
		}

		return result;
	}
259

260
	private onReplace(tree: ITree, event: IKeyboardEvent): boolean {
J
Johannes Rieken 已提交
261
		let input = <SearchResult>tree.getInput();
262 263
		let result = false;
		let element = tree.getFocus();
S
Sandeep Somavarapu 已提交
264
		if (element instanceof Match && input.searchModel.isReplaceActive()) {
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
			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;
	}

283 284 285 286 287
	protected onUp(tree: ITree, event: IKeyboardEvent): boolean {
		if (tree.getNavigator().first() === tree.getFocus()) {
			this.viewlet.moveFocusFromResults();
			return true;
		}
R
roblou 已提交
288 289 290 291 292 293 294 295 296 297 298 299

		const result = super.onUp(tree, event);
		let focus = tree.getFocus();
		this.selectOnScroll(tree, focus, event);
		return result;
	}

	protected onDown(tree: ITree, event: IKeyboardEvent): boolean {
		const result = super.onDown(tree, event);
		let focus = tree.getFocus();
		this.selectOnScroll(tree, focus, event);
		return result;
300
	}
301

J
Johannes Rieken 已提交
302
	protected onSpace(tree: ITree, event: IKeyboardEvent): boolean {
303 304 305 306 307 308
		let element = tree.getFocus();
		if (element instanceof Match) {
			return this.onEnter(tree, event);
		}
		super.onSpace(tree, event);
	}
R
roblou 已提交
309 310

	private selectOnScroll(tree: ITree, focus: any, event: IKeyboardEvent): void {
311 312 313 314
		this._arrowKeyThrottler.trigger(() => this.doSelectOnScroll(tree, focus, event));
	}

	private doSelectOnScroll(tree: ITree, focus: any, event: IKeyboardEvent): void {
R
roblou 已提交
315 316 317 318 319 320
		if (focus instanceof Match) {
			this.onEnter(tree, event);
		} else {
			tree.setSelection([focus]);
		}
	}
321 322
}

323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
class DumbThrottler {
	private waiting = false;

	private callback: Function;

	constructor(private timeout: number) {
	}

	trigger(callback: Function): void {
		if (this.waiting) {
			this.callback = callback;
		} else {
			callback();
			this.waiting = true;
			setTimeout(() => {
				this.waiting = false;
				if (this.callback) {
					this.callback();
					this.callback = null;
				}
			}, this.timeout);
		}
	}
}

348 349 350 351 352 353
export class SearchFilter implements IFilter {

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