searchResultsView.ts 12.3 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
		}

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

		return undefined;
86 87 88 89 90
	}
}

class SearchActionProvider extends ContributableActionProvider {

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

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

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

			return actions;
		});
	}
}

export class SearchRenderer extends ActionsRenderer {

122
	constructor(actionRunner: IActionRunner, viewlet: SearchViewlet, @IWorkspaceContextService private contextService: IWorkspaceContextService,
J
Johannes Rieken 已提交
123
		@IInstantiationService private instantiationService: IInstantiationService) {
124
		super({
125
			actionProvider: instantiationService.createInstance(SearchActionProvider, viewlet),
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
			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 已提交
145
				const label = this.instantiationService.createInstance(FileLabel, left, void 0);
146
				label.setFile(fileMatch.resource());
147

148
				return () => label.dispose();
149 150 151 152 153
			};

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

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

			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 已提交
168
			let match = <Match>element;
169
			let elements: string[] = [];
S
Sandeep Somavarapu 已提交
170
			let preview = match.preview();
171 172 173

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

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

			$('a.plain')
				.innerHtml(elements.join(strings.empty))
S
Sandeep Somavarapu 已提交
189
				.title((preview.before + (showReplaceText ? match.replaceString : preview.inside) + preview.after).trim().substr(0, 999))
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
				.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 已提交
210 211
			let match = <Match>element;
			let input = <SearchResult>tree.getInput();
212
			if (input.searchModel.isReplaceActive()) {
S
Sandeep Somavarapu 已提交
213 214
				let preview = match.preview();
				return nls.localize('replacePreviewResultAria', "Replace preview result, {0}", preview.before + match.replaceString + preview.after);
S
Sandeep Somavarapu 已提交
215
			}
S
Sandeep Somavarapu 已提交
216
			return nls.localize('searchResultAria', "{0}, Search result", match.text());
217
		}
M
Matt Bierner 已提交
218
		return undefined;
219 220 221 222 223
	}
}

export class SearchController extends DefaultController {

224 225
	private _gotArrowUpKeyUp = true;
	private _gotArrowDownKeyUp = true;
226

227
	constructor(private viewlet: SearchViewlet, @IInstantiationService private instantiationService: IInstantiationService) {
228 229 230
		super({ clickBehavior: ClickBehavior.ON_MOUSE_DOWN });

		if (platform.isMacintosh) {
A
Alexandru Dima 已提交
231 232
			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));
233
		} else {
A
Alexandru Dima 已提交
234 235
			this.downKeyBindingDispatcher.set(KeyCode.Delete, (tree: ITree, event: any) => { this.onDelete(tree, event); });
			this.upKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.Enter, this.onEnter.bind(this));
236 237
		}

238 239 240
		this.upKeyBindingDispatcher.set(KeyCode.UpArrow, this.upKeyArrowUp.bind(this));
		this.upKeyBindingDispatcher.set(KeyCode.DownArrow, this.upKeyArrowDown.bind(this));

241 242
		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 已提交
243
		this.downKeyBindingDispatcher.set(KeyCode.Escape, (tree: ITree, event: any) => { this.onEscape(tree, event); });
244 245 246 247 248 249 250 251 252 253 254
	}

	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 已提交
255
		let input = <SearchResult>tree.getInput();
256 257
		let result = false;
		let element = tree.getFocus();
258
		if (element instanceof FileMatch ||
J
Johannes Rieken 已提交
259
			(element instanceof Match && input.searchModel.isReplaceActive())) {
260 261 262 263 264 265
			new RemoveAction(tree, element).run().done(null, errors.onUnexpectedError);
			result = true;
		}

		return result;
	}
266

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

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

		const result = super.onUp(tree, event);
297 298 299 300 301 302 303

		// Ignore keydown events while the key is held
		if (this._gotArrowUpKeyUp) {
			this.doSelectOnScroll(tree, tree.getFocus(), event);
			this._gotArrowUpKeyUp = false;
		}

R
roblou 已提交
304 305 306
		return result;
	}

307 308 309 310 311 312 313 314 315 316 317 318
	private upKeyArrowUp(tree: ITree, event): boolean {
		this.doSelectOnScroll(tree, tree.getFocus(), event);
		this._gotArrowUpKeyUp = true;
		return true;
	}

	private upKeyArrowDown(tree: ITree, event): boolean {
		this.doSelectOnScroll(tree, tree.getFocus(), event);
		this._gotArrowDownKeyUp = true;
		return true;
	}

R
roblou 已提交
319 320
	protected onDown(tree: ITree, event: IKeyboardEvent): boolean {
		const result = super.onDown(tree, event);
321 322 323 324 325 326 327

		// Ignore keydown events while the key is held
		if (this._gotArrowDownKeyUp) {
			this.doSelectOnScroll(tree, tree.getFocus(), event);
			this._gotArrowDownKeyUp = false;
		}

R
roblou 已提交
328
		return result;
329
	}
330

J
Johannes Rieken 已提交
331
	protected onSpace(tree: ITree, event: IKeyboardEvent): boolean {
332 333 334 335 336
		let element = tree.getFocus();
		if (element instanceof Match) {
			return this.onEnter(tree, event);
		}
		super.onSpace(tree, event);
M
Matt Bierner 已提交
337
		return false;
338
	}
R
roblou 已提交
339

340
	private doSelectOnScroll(tree: ITree, focus: any, event: IKeyboardEvent): void {
R
roblou 已提交
341 342 343 344 345 346
		if (focus instanceof Match) {
			this.onEnter(tree, event);
		} else {
			tree.setSelection([focus]);
		}
	}
347 348 349 350 351 352 353 354
}

export class SearchFilter implements IFilter {

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