searchResultsView.ts 11.0 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 152 153 154 155 156 157 158 159 160 161 162 163 164
			};

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

				return new CountBadge(right, len, len > 1 ? nls.localize('searchMatches', "{0} matches found", len) : nls.localize('searchMatch', "{0} match found", len));
			};

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

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

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

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

export class SearchController extends DefaultController {

220
	constructor(private viewlet: SearchViewlet, @IInstantiationService private instantiationService: IInstantiationService) {
221 222 223
		super({ clickBehavior: ClickBehavior.ON_MOUSE_DOWN });

		if (platform.isMacintosh) {
A
Alexandru Dima 已提交
224 225
			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));
226
		} else {
A
Alexandru Dima 已提交
227 228
			this.downKeyBindingDispatcher.set(KeyCode.Delete, (tree: ITree, event: any) => { this.onDelete(tree, event); });
			this.upKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.Enter, this.onEnter.bind(this));
229 230
		}

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

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

		return result;
	}
256

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

280 281 282 283 284 285 286
	protected onUp(tree: ITree, event: IKeyboardEvent): boolean {
		if (tree.getNavigator().first() === tree.getFocus()) {
			this.viewlet.moveFocusFromResults();
			return true;
		}
		return super.onUp(tree, event);
	}
287

J
Johannes Rieken 已提交
288
	protected onSpace(tree: ITree, event: IKeyboardEvent): boolean {
289 290 291 292 293 294
		let element = tree.getFocus();
		if (element instanceof Match) {
			return this.onEnter(tree, event);
		}
		super.onSpace(tree, event);
	}
295 296 297 298 299 300 301 302
}

export class SearchFilter implements IFilter {

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